I am writing unit tests for sagas that call internal helper functions to improve readability of the code. The issue is that by moving code to an internal helper function, I am now unable to test its behavior without exporting it (and exposing internals). For example:
export function* testExample1() {
yield put({ type: 'TEST_ACTION_1' });
yield helperNotExported();
yield put({ type: 'TEST_ACTION_3' });
}
function* helperNotExported() {
yield put({ type: 'TEST_ACTION_2' });
}
describe('testExample 1', () => {
const saga = sagas.testExample1();
expect(saga.next().value).toEqual(put({ type: 'TEST_ACTION_1' }));
// this line fails
expect(saga.next().value).toEqual(put({ type: 'TEST_ACTION_2' }));
expect(saga.next().value).toEqual(put({ type: 'TEST_ACTION_3' }));
});
I can get it to "sort of" work by exporting the internal helper function, but then I have to also test that helper function independently.
export function* testExample2() {
yield put({ type: 'TEST_ACTION_1' });
yield call(helperIsExported);
yield put({ type: 'TEST_ACTION_3' });
}
export function* helperIsExported() {
yield put({ type: 'TEST_ACTION_2' });
}
And the tests that work:
describe('testExample 2', () => {
const saga = sagas.testExample2();
expect(saga.next().value).toEqual(put({ type: 'TEST_ACTION_1' }));
expect(saga.next().value).toEqual(call(sagas.helperIsExported));
expect(saga.next().value).toEqual(put({ type: 'TEST_ACTION_3' }));
});
Notice that the TEST_ACTION_2 put call is never tested in this case.
Is there a better way to write these tests to accomplish this without having to export every function that any saga uses?
Just a random guy making an observation.
It seems that testing the call effect for your helperIsExported function is in line with the saga philosophy of only invoking effects through the saga effects helpers (e.g. put, call, fork etc). So I'm not sure if there would be a "better way" to accomplish your testing effort. I feel that is the way you're guided towards given the saga API.
On a related note, I didn't include test coverage for sagas much b/c of needing to test each effect in sequence. However, redux-saga-test-plan allows for sequenced tests and non-sequenced tests (https://redux-saga-test-plan.jeremyfairbank.com/). So instead of testing effects got called in a certain sequence, I'm testing that given a starting state, that the ending state is achieved (https://redux-saga-test-plan.jeremyfairbank.com/integration-testing/state.html). Might be worth a consideration.
Verifying beginning and end state isn't the only option for adding test coverage to sagas via redux-saga-test-plan, but it's a strategy I'm inclined to. Maybe it'll suit your needs as well.
Here is an excellent article about testing approaches - http://blog.scottlogic.com/2018/01/16/evaluating-redux-saga-test-libraries.html
I strongly advise to use a different approach than presented at the moment in the docs, because it's tightly coupled with internal structure and results in problems described by you.
I'd go for more integration-like testing. Setup a simple part of your app in test, run your saga and assert on actual app effects (this might be an updated state shape, outgoing request, or something rendered by an isolated component), rather than effect objects. It might be a little bit harder with latest redux-saga, but should be WAY easier (regarding network requests) with redux-saga@beta. I consider this version stable, just lock your installed version in package.json. If you use the beta you will be able to use a new effectMiddlewares feature to mock your network etc responses more easily. Please checkout the release note of the beta.
Thanks! Redux-saga-test-plan seems to be the best of the options available.
Most helpful comment
Here is an excellent article about testing approaches - http://blog.scottlogic.com/2018/01/16/evaluating-redux-saga-test-libraries.html
I strongly advise to use a different approach than presented at the moment in the docs, because it's tightly coupled with internal structure and results in problems described by you.
I'd go for more integration-like testing. Setup a simple part of your app in test, run your saga and assert on actual app effects (this might be an updated state shape, outgoing request, or something rendered by an isolated component), rather than effect objects. It might be a little bit harder with latest
redux-saga, but should be WAY easier (regarding network requests) withredux-saga@beta. I consider this version stable, just lock your installed version inpackage.json. If you use the beta you will be able to use a neweffectMiddlewaresfeature to mock your network etc responses more easily. Please checkout the release note of the beta.