saga-test-stub
v3.7.0
Published
Saga test with stubbing capabilities
Downloads
65
Maintainers
Readme
Redux Saga Test Stub
What you want
- Test your sagas like you would do with non-saga functions
- Test complex sagas (branches, loops, and whatnot) like this one:
export function* drive(destination: string): Iterator<Effect, string, any> {
const origin = yield select(currentPosition)
const route = yield call(getRoute, origin, destination);
if (route === 'unknown') {
return 'ask for direction';
}
let position = origin;
do {
if (position == origin) {
yield put({ type: 'at origin' });
}
const lights = yield select(trafficLight);
if (lights.green == true) {
yield put({ type: 'keep driving' });
}
if (lights.red == true) {
yield put({ type: 'apply brakes' });
}
if (lights.yellow == true) {
const distance = yield select(distanceToLine);
if (distance < 1) {
yield put({ type: 'apply brakes' });
}
else {
yield put({ type: 'keep driving' });
}
}
position = yield select(currentPosition);
if (position == 'bermuda triangle') {
throw new Error('we are lost');
}
} while (position != destination);
return 'we are at destination';
}
What you get
Stubs
when(saga).yields(effect | (effect)=>boolean).doNext(...values)
tell to tester what to return when iteration yields an effectwhen(saga).selects(selector,...args).doNext(...values)
shortcut to when(saga).yields(select())when(saga).calls(fn,...args).doNext(...values)
shortcut to when(saga).yields(call())when(saga).yields(effect | (effect)=>boolean).integrate(sagaStub)
tell to tester to run a saga when iteration yields one (integration test)when(saga).yields(effect | (effect)=>boolean).throw(Error)
tell to tester to throw an Error
Assertions
run(saga).expecting(toYield(...effects | ...(effect)=>boolean))
run saga and expect effects to be yielded in orderrun(saga).expecting(...effects | ...(effect)=>boolean)
same as expecting(toYield(effects))run(saga).expecting(toYieldStrict(...effects | ...(effect)=>boolean))
run saga and expect effects to be yielded strictly in orderrun(saga).expecting(toBeDoneAfter(effect | (effect)=>boolean))
run saga, expect effect to be yielded and to be done afterrun(saga).expecting(toReturn(value | (value)=>boolean))
run saga and expect return value when donerun(saga).expectingDone((value | (value)=>boolean)?)
same as expecting(toReturn()), value is optionalrun(saga).expecting(toThrowError(message))
run saga and expect an error to be thrownrun(saga).expecting(not(qualifier)))
can be used with one of the previous assertion to negate it
Jest assertions
expect(saga).toYield(...effects | ...(effect)=>boolean)
same asrun(saga).expecting(toYield())
expect(saga).toPut(...actions)
shortcut for expect.toYield(...put())expect(saga).toCall(fn,...args)
shortcut for expect.toYield(call(fn,args))expect(saga).toYieldStrict(...effects | ...(effect)=>boolean)
same asrun(saga).expecting(toYieldStrict())
expect(saga).toBeDoneAfter(effect | (effect)=>boolean)
same asrun(saga).expecting(toBeDoneAfter())
expect(saga).toYieldLast(...effects | ...(effect)=>boolean)
same as toBeDoneAfter but with a list of effectsexpect(saga).toReturn(value | (value)=>boolean)
run saga and expect return value when doneexpect(saga).toBeDone((value | (value)=>boolean)?)
same as toReturn, value is optional
Note: with jest you can use matchers like expect.objetContaining
and expect.arrayContaining
What your unit tests would be
First initialize your saga
import { stub } from "saga-test-stub";
//with jest: import { stub } from "saga-test-stub/jest";
const saga = stub(drive);
Stub effects
import { when } from "saga-test-stub";
//with jest-when: import { when } from "saga-test-stub/jest-when";
when(saga).yields(call(getRoute, 'point A', 'point D')).doNext('go to B, then C and you will be at D');
when(saga).yields(select(trafficLight)).doNext(lights);
Do assertion
run(saga).expecting(toYield(put({ type: 'keep driving' })));
run(saga).expecting(toReturn('ask for direction'));
//jest
expect(saga).toYield(put({ type: 'keep driving' }));
expect(saga).toReturn('ask for direction');
Shorcuts
when(saga).calls(getRoute, 'point A', 'point D').doNext('go to B, then C and you will be at D');
when(saga).selects(trafficLight).doNext(lights);
//jest
expect(saga).toYield(put({ type: 'keep driving' }));
expect(saga).toSelect(distanceToLine);
Error handling
when(saga).selects(trafficLight).throw(new Error('no electricity, do a hard stop'));
run(saga).expecting(toThrowError('we are lost'));
//for jest, you'll get more debug information using run(saga), but if you absolutely want to do expect(), this is an option:
expect(() => saga.run()).toThrowError('we are lost');
Branches
When testing a positive branch, you have to make sure to test the negative one, ex:
const a = yield select(something);
if (a == 1){
yield put(someAction);
}
//test
when(saga).selects(something).doNext(1);
run(saga).expecting(toPut(someAction)); //positive branch
If you don't test the negative branch (a!=1), the previous test will be successful even if you delete the if
const a = yield select(something);
// if (a == 1){
yield put(someAction);
// }
So, you should also have in your suite:
when(saga).selects(something).doNext(2);
run(saga).expecting(not(toPut(someAction)));
but this is still weak, there is a lot of way to break the code and you're test will be successful, so better use toBeDoneAfter
or toYieldStrict
Use toBeDoneAfter
if there is no yield after the branch
const a = yield select(something);
if (a == 1){
yield put(someAction);
}
return;
//negative branch
when(saga).selects(something).doNext(2);
run(saga).expecting(toBeDoneAfter(select(something)));
Use toYieldStrict
if there is a yield after the branch
const a = yield select(something);
if (a == 1){
yield put(someAction);
}
yield take(aBreak);
//negative branch
when(saga).selects(something).doNext(2);
run(saga).expecting(toYieldStrict(select(something),take(aBreak)));
Loops
stubbing allows to give a list of effects to yield, so to simply test the drive saga, you can do:
when(saga).yields(select(currentPosition)).doNext('point A', 'point B', 'point C', 'point D');
in this the saga will go 4 times trough the loop
Complete test
describe('demo', () => {
const saga = stub(drive, 'point D');
let lights: any;
beforeEach(() => {
when(saga).yields(select(currentPosition)).doNext('point A', 'point B', 'point C', 'point D');
lights = { green: false, red: false, yellow: false }
when(saga).selects(trafficLight).doNext(lights);
});
describe('when route is unknown', () => {
beforeEach(() => {
when(saga).yields(call(getRoute, 'point A', 'point D')).doNext('unknown');
});
it('should do nothing after asking for route', () => {
run(saga).expecting(toBeDoneAfter(call(getRoute, 'point A', 'point D')));
});
it('should return cannot drive there', () => {
run(saga).expecting(toReturn('ask for direction'));
});
});
describe('when a route is found', () => {
beforeEach(() => {
when(saga).yields(call(getRoute, 'point A', 'point D')).doNext('go to B, then C and you will be at D');
});
describe('when traffic light is green', () => {
beforeEach(() => {
lights.green = true;
});
it('should keep driving', () => {
run(saga).expecting(toYield(put({ type: 'keep driving' })));
});
});
describe('when traffic light is yellow', () => {
beforeEach(() => {
lights.yellow = true;
});
describe('when distance is less than 1', () => {
beforeEach(() => {
when(saga).selects(distanceToLine).doNext(0.9);
});
it('should apply brakes', () => {
run(saga).expecting(toYield(put({ type: 'apply brakes' })));
});
});
describe('when distance is more than 1', () => {
beforeEach(() => {
when(saga).selects(distanceToLine).doNext(1.1);
});
it('should keep driving', () => {
run(saga).expecting(toYield(put({ type: 'keep driving' })));
});
});
});
describe('when route goes by bermuda triangle', () => {
beforeEach(() => {
when(saga).yields(select(currentPosition)).doNext('point A', 'bermuda triangle', 'point D');
});
it('should throw an error', () => {
run(saga).expecting(toThrowError('we are lost'));
});
});
});
});
What the tester really do
Simply tries to go as far as possible in the iteration with the stub information provided until it match expectation
What it looks like when my code is broken
You get a report with yielded effects and what action the tester took (next() or next(stubbedValue))
FAIL tests/saga.spec.ts
fritkot saga with jest
when sadly closed
✓ should wait (16 ms)
✓ should be done (7 ms)
when open for business
✓ should ask for the bill and thank the chef (13 ms)
✓ should ask the bill (11 ms)
✓ should thank the chef (10 ms)
when world is sad and there is no more hot sauces
✓ should ask for non hot sauce and pick the first one (11 ms)
when there is Samourai sauce
✕ should ask for Samourai (17 ms)
when there is not Samourai sauce
✓ should ask for the second one (10 ms)
when there is 2 fries left
✓ should eat the fries and be sad (28 ms)
● fritkot saga with jest › when open for business › when there is Samourai sauce › should ask for Samourai
Expected effects were not yielded, this happened:
YIELD {"@@redux-saga/IO": true, "combinator": false, "payload": {"args": [], "selector": [Function getFritkot]}, "type": "SELECT"}
NEXT ({"open":true})
YIELD {"@@redux-saga/IO": true, "combinator": false, "payload": {"action": {"type": "Frites"}, "channel": undefined}, "type": "PUT"}
NEXT ()
YIELD {"@@redux-saga/IO": true, "combinator": false, "payload": {"args": [true], "context": null, "fn": [Function getSauces]}, "type": "CALL"}
NEXT (["Piri-piri","Samourai"])
YIELD {"@@redux-saga/IO": true, "combinator": false, "payload": {"action": {"type": "Samourai"}, "channel": undefined}, "type": "PUT"}
NEXT ()
YIELD {"@@redux-saga/IO": true, "combinator": false, "payload": {"args": [], "selector": [Function isPlateEmpty]}, "type": "SELECT"}
NEXT (true)
YIELD {"@@redux-saga/IO": true, "combinator": false, "payload": {"action": {"type": "Snif ! Y'a pu d'frite"}, "channel": undefined}, "type": "PUT"}
NEXT ()
65 |
66 | it("should ask for Samourai", () => {
> 67 | expect(saga).toPut({ type: 'Samoura' });
| ^
68 | });
69 | });
70 |
at Object.<anonymous> (tests/saga.spec.ts:67:30)
What is in the future
- add jasmine support
What is in the past
3.7.0
- update core semantic: expecting(qualifier)
- add core qualifiers:
- toYield,
- toYieldStrict
- toBeDoneAfter
- toReturn
- toThrowError
- possibility to negate all previous assertion with not()
- add jest assertions
- toYieldStrict
- toBeDoneAfter
- toReturn
3.6.0
- jest 29.4 support
3.5.0
- add throw capability to stubbing
3.4.1
- fix/improve fail report
3.4.0
- add expect.toYieldLast
- fix expect.toPut signature (extends Action)
- fix expect.toCall signature
- fix error message when expection in saga
- fix when() signature and generic (add parameter as function)
3.3.0
- add when.yields.integrate
3.2.0
- add shortcuts: when.selects, when.calls and expect.toCall
3.1.1
- fix jest.not messaging
- fix jest --expand messaging
3.1.0 (=3.0.2+fix semver)
- fix peer dependencies
- add jest-when support
3.0.1 (=3.0.0+README)
- add typescript support
- match effect with (effect)=>boolean
- improve stringify(effect): reselect and other are not showing
2.0.0
- add flexibility (match effects by function)