Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Question: How to properly test typed-redux-saga #666

Open
ACHP opened this issue Aug 12, 2022 · 0 comments
Open

Question: How to properly test typed-redux-saga #666

ACHP opened this issue Aug 12, 2022 · 0 comments

Comments

@ACHP
Copy link

ACHP commented Aug 12, 2022

Hi,
First of all, thank you for this great library, it helped me improve the typing and quality of my codebase a lot !
But still do have some question about how to properly test saga that uses typed-redux-saga instead of regular redux-saga.

Let's consider this saga

import * as untypedReduxSaga from "redux-saga/effects";

export async function randomPromise(): Promise<number>{
    return 42;
}

export function* untypedTodosSaga(){
    const {p1} = yield untypedReduxSaga.all({
        p1: untypedReduxSaga.call(randomPromise),
    })
    return p1;
}

We can test it pretty easily using this test ( And the test passes ✅ )

import * as untypedReduxSaga from "redux-saga/effects";
import {randomPromise, untypedTodosSaga} from "./todosSaga";

test('regular redux saga', ()=>{
    const gen = untypedTodosSaga();
    expect(gen.next().value).toEqual(untypedReduxSaga.all({
            p1: untypedReduxSaga.call(randomPromise),
        })
    );
    expect(gen.next({p1: 44}).value).toEqual(44);
})

But if the original codebase we replace regular redux-saga) (untypedReduxSaga) by the typed version the test fail 🟥

import * as typedReduxSaga from "typed-redux-saga";

export async function randomPromise(): Promise<number>{
    return 42;
}

export function* typedTodosSaga(){
    const {p1} = yield* typedReduxSaga.all({
        p1: typedReduxSaga.call(randomPromise),
    })
    return p1;
}
- Expected  - 10
+ Received  +  1

  Object {
    "@@redux-saga/IO": true,
    "combinator": true,
    "payload": Object {
-     "p1": Object {
-       "@@redux-saga/IO": true,
-       "combinator": false,
-       "payload": Object {
-         "args": Array [],
-         "context": null,
-         "fn": [Function randomPromise],
-       },
-       "type": "CALL",
-     },
+     "p1": Object {},
    },
    "type": "ALL",
  }

And the reason behind is that the yield* actually "unwrap" the all combinator, and therefore returns the actual JSON payload of the effect, but the nested call function itself remains a generator around the "real" redux-saga call.

Meaning that we can actually test that the all effect has been yield but we can't check the content of this all effect, greatly degrading the interest of our test.

We can't update the code to unwrap the call effect by hand without losing types inference.

export function* typedTodosSaga(){
    const {p1} = yield* typedReduxSaga.all({
        p1: typedReduxSaga.call(randomPromise).next().value, // 🟥  p1 => unknown
    })
    return p1;
}

Workaround

The workaround I use for now is to manually "unwrap" the effect if it is a combinatorEffect (cf: all | race) using the following function.

import type {CombinatorEffect, CombinatorEffectDescriptor, Effect} from '@redux-saga/types';
import {SagaGenerator} from "typed-redux-saga";

import {map} from 'ramda';

function isCombinatorEffect<T = any, P = any>(
    effect: Effect<T, P | CombinatorEffectDescriptor<P>> | unknown,
): effect is CombinatorEffect<T, P> {
    return (effect as Effect<T, P | CombinatorEffectDescriptor<P>>).combinator;
}

export function unwrapCombinators<E extends Effect<T, P> | unknown, T, P extends SagaGenerator<any> | any>(step: E): E {
    const mapper = (value: P) => {
        if (typeof (value as SagaGenerator<any>).next === 'function') {
            return (value as SagaGenerator<any>).next().value;
        } else {
            return value;
        }
    };
    if (isCombinatorEffect(step)) {
        // @ts-ignore honestly this is way to complex to type this :/
        const newPayload = map(mapper, step.payload);

        return {...step, payload: newPayload};
    } else {
        return step;
    }
}

My test now looks like this, and passes ✅ , and the saga is still correctly typed.

test('typed redux saga (using unwrapCombinator)', ()=>{
    const gen = typedTodosSaga();
    expect(unwrapCombinators(gen.next().value)).toEqual(untypedReduxSaga.all({
            p1: untypedReduxSaga.call(randomPromise),
        })
    );
    expect(gen.next({p1: 44}).value).toEqual(44);
})

Suggestions

  • Maybe the combinators effect can always recursively unwrap nested effects
  • Or, this library can provide a helper function like unwrapCombinators to help deal with it
  • Or provide/document a better alternative
@ACHP ACHP changed the title Question: Usage of typed-redux-saga regarding test Question: How to properly test typed-redux-saga Aug 16, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant