Search code examples
javascriptreduxredux-sagaredux-saga-test-plan

How to test sagas when there are functions not using 'yield' or an effect like 'call' or 'put'?


In all the examples I've found everyone is using yield call(), yield put(), etc. on their sagas. Right now I have a saga that just executes a function without using yield call(). This function is executed after a select effect and before a call effect (see code below for the service variable. This function returns an instance of a class, it's not a network request or a promise.

The saga works fine, but I'm not sure how to test it like this. Using redux-sagas-test-plan while having the effects works fine, but as soon as I remove the effect (which I remove from the .provide(), the test fails.

Saga

export function* getDetails() {
    try {
        const config = yield select(getProperties());
        const service = getService(config);
        const data = yield call([service, service.getDetails]);
        yield put(success(data));
    } catch(e) {
        yield put(failure());
    }
}

Test

import { getDetails as detailsSaga } from '...';
const data = {};

it('should succeed getting details', async () => {
        await expectSaga(detailsSaga)
            .provide([
                [select(getProperties), {}],
                [call([serviceMock, serviceMock.getDetails]), data]
            ])
            .put(success(data))
            .dispatch(fetchDetails())
            .silentRun();
    });

The expected result from the test is tu have the success(data) creator to execute but instead I get the failure() creator as the actual value.

Expected
    --------
    { '@@redux-saga/IO': true,
      combinator: false,
      type: 'PUT',
      payload: 
       { channel: undefined,
         action: 
          { type: 'FETCH_DETAILS_SUCCESS',
            data: { } } } }

    Actual:
    ------
    1. { '@@redux-saga/IO': true,
      combinator: false,
      type: 'PUT',
      payload: 
       { channel: undefined,
         action: { type: 'FETCH_DETAILS_FAILURE' } } }

Solution

  • Based on other examples I found, this was my solution:

    import { call } from 'redux-saga-test-plan/matchers';
    
    beforeEach(() => {
      service = getService({});
      data = { /* ... */ });
    });
    
    it('should succeed getting details', async () => {
      await expectSaga(detailsSaga)
        .provide([
          [select(getProperties), {}],
          [call.fn(service.getDetails), data]
        ])
        .put(success(data))
        .dispatch(fetchDetails())
        .silentRun();
    });
    

    I have to use the call effect from redux-saga-test-plan/matchers and create an actual instance returned by getService. For some reason a mock doesn't work here.

    The details of why this works are not that clear to me, but I'm writing this answer just in case someone is trying to achieve this as well. Note: I agree with the other answer, that all functions should be called using yield call but I've been asked not to do that here.