Static Providers
You can provide mock values in a terse manner via static providers. Pass in
an array of tuple pairs (array pairs) into the provide
method. For each pair,
the first element should be a matcher for matching the effect and the second
effect should be the mock value you want to provide. You can use effect creators
from redux-saga/effects
as matchers or import matchers from
redux-saga-test-plan/matchers
. The benefit of using Redux Saga Test Plan's
matchers is that they also offer partial matching. For example, you can use
call.fn
to match any calls to a function without regard to its arguments.
import { call, put, select } from 'redux-saga/effects';
import { expectSaga } from 'redux-saga-test-plan';
import * as matchers from 'redux-saga-test-plan/matchers';
import api from 'my-api';
import * as selectors from 'my-selectors';
function* saga() {
const id = yield select(selectors.getId);
const user = yield call(api.fetchUser, id);
yield put({ type: 'RECEIVE_USER', payload: user });
}
it('provides a value for the API call', () => {
return expectSaga(saga)
.provide([
// Use the `select` effect creator from Redux Saga to match
[select(selectors.getId), 42],
// Use the `call.fn` matcher from Redux Saga Test Plan
[matchers.call.fn(api.fetchUser), { id: 42, name: 'John Doe' }],
])
.put({
type: 'RECEIVE_USER',
payload: { id: 42, name: 'John Doe' },
})
.run();
});
Matchers
Inside the redux-saga-test-plan/matchers
module, there are matchers for most
of the effect creators available in Redux Saga. You can reference effect
creators in Redux Saga's docs
here.
actionChannel(pattern, [buffer])
apply(context, fn, args)
call([context, fn], ...args)
call(fn, ...args)
cancel(task)
cancelled()
cps([context, fn], ...args)
cps(fn, ...args)
flush(channel)
fork([context, fn], ...args)
fork(fn, ...args)
getContext(prop)
join(task)
put(action)
put.resolve(action)
race(effects)
select(selector, ...args)
setContext(props)
spawn([context, fn], ...args)
spawn(fn, ...args)
take(pattern)
take.maybe(pattern)
Partial Matchers
Sometimes you're not interested in matching a call
effect with exact arguments
or a put
effect with a particular action payload.
Instead you only want to match a call
to a particular function or match a
put
with a particular action type. You can handle these situations with
partial matchers.
The following assertions have a like
method along with convenient helper
methods for partially matching assertions:
actionChannel
apply
call
cps
fork
put
put.resolve
select
spawn
NOTE: the like
method requires knowledge of the properties on effects such
as the fn
property of call
and the action
property of put
. Essentially,
like
allows you to match effects with certain properties without worrying
about the other properties. Therefore, you can match a call
by fn
without
worrying about the args
property.
In addition to like
, there are other some common helper methods like fn
and
actionType
available, appropriate to the kind of effect:
Method | Description |
---|---|
actionChannel.pattern |
Match actionChannel by pattern . Useful if you use custom buffers with actionChannel . |
apply.fn |
Match apply by fn . |
call.fn |
Match call by fn . |
cps.fn |
Match cps by fn . |
fork.fn |
Match fork by fn . |
put.actionType |
Match put by action.type . |
put.resolve.actionType |
Match put.resolve by action.type . |
select.selector |
Match select by selector function. |
spawn.fn |
Match spawn by fn . |
Ordering
Providers are checked from left to right (or top to down depending on how you look at it). The first provider to match an effect is used, skipping subsequent providers. If no providers match, then the effect is handled by Redux Saga as normal.
import { call, put, select } from 'redux-saga/effects';
import * as matchers from 'redux-saga-test-plan/matchers';
import api from 'my-api';
import * as selectors from 'my-selectors';
function* saga() {
const user = yield call(api.findUser, 1);
const dog = yield call(api.findDog);
const greeting = yield call(api.findGreeting);
const otherData = yield select(selectors.getOtherData);
yield put({
type: 'DONE',
payload: { user, dog, greeting, otherData },
});
}
const fakeUser = { name: 'John Doe' };
const fakeDog = { name: 'Tucker' };
const fakeOtherData = { foo: 'bar' };
it('takes multiple providers and composes them', () => {
return expectSaga(saga)
.provide([
[matchers.call.fn(api.findUser), fakeUser],
[matchers.call.fn(api.findDog), fakeDog],
[select(selectors.getOtherData), fakeOtherData],
])
.put({
type: 'DONE',
payload: {
user: fakeUser,
dog: fakeDog,
greeting: 'hello',
otherData: fakeOtherData,
},
})
.run();
});
Throw Errors
You can simulate errors with static providers via the throwError
function from
the redux-saga-test-plan/providers
module. When providing an error, wrap it
with a call to throwError
to let Redux Saga Test Plan know that you want to
simulate a thrown error.
import { call, put } from 'redux-saga/effects';
import * as matchers from 'redux-saga-test-plan/matchers';
import { throwError } from 'redux-saga-test-plan/providers';
import api from 'my-api';
function* userSaga(id) {
try {
const user = yield call(api.fetchUser, id);
yield put({ type: 'RECEIVE_USER', payload: user });
} catch (e) {
yield put({ type: 'FAIL_USER', error: e });
}
}
it('handles errors', () => {
const error = new Error('error');
return expectSaga(userSaga)
.provide([
[matchers.call.fn(api.fetchUser), throwError(error)],
])
.put({ type: 'FAIL_USER', error })
.run();
});
Static Providers with Dynamic Values
Static providers can provide dynamic values too. Instead of supplying a static
value, you can supply a function that produces the value. This function takes as
arguments the matched effect as well as a next
function. Additionally, you
must wrap the function with a call to the dynamic
function from the
redux-saga-test-plan/providers
module. Inside the provider function, you can
inspect the effect further and return a mock value or return a call to the
next
function. Returning a call to the next
function will tell Redux Saga
Test Plan to try the next provider, similar to a middleware stack. If there are
no more providers, then Redux Saga Test Plan will let Redux Saga handle the
effect.
import { call, put } from 'redux-saga/effects';
import * as matchers from 'redux-saga-test-plan/matchers';
import { dynamic } from 'redux-saga-test-plan/providers';
const add2 = a => a + 2;
function* someSaga() {
const x = yield call(add2, 4);
const y = yield call(add2, 6);
const z = yield call(add2, 8);
yield put({ type: 'DONE', payload: x + y + z });
}
const provideDoubleIf6 = ({ args: [a] }, next) => (
// Check if the first argument is 6
a === 6 ? a * 2 : next()
);
const provideTripleIfGt4 = ({ args: [a] }, next) => (
// Check if the first argument is greater than 4
a > 4 ? a * 3 : next()
);
it('works with dynamic static providers', () => {
return expectSaga(someSaga)
.provide([
[matchers.call.fn(add2), dynamic(provideDoubleIf6)],
[matchers.call.fn(add2), dynamic(provideTripleIfGt4)],
])
.put({ type: 'DONE', payload: 42 })
.run();
});
Other Examples
Parallel Effects via all
Providers work on effects yielded inside an all
effect:
import { put, select } from 'redux-saga/effects';
import { expectSaga } from 'redux-saga-test-plan';
import { selectors } from 'my-selectors';
function* saga() {
const [name, age] = yield all([
select(selectors.getName),
select(selectors.getAge),
]);
yield put({ type: 'USER', payload: { name, age } });
}
it('provides values for effects inside arrays', () => {
return expectSaga(saga)
.provide([
[select(selectors.getName), 'Tucker'],
[select(selectors.getAge), 11],
])
.put({
type: 'USER',
payload: { name: 'Tucker', age: 11 },
})
.run();
});
Parallel Effects via an Array
Providers work on effects yielded inside an array too. NOTE: yielding an array is deprecated in Redux Saga, so this functionality will be removed when Redux Saga removes support for yielded arrays.
import { put, select } from 'redux-saga/effects';
import { expectSaga } from 'redux-saga-test-plan';
import { selectors } from 'my-selectors';
function* saga() {
const [name, age] = yield [
select(selectors.getName),
select(selectors.getAge),
];
yield put({ type: 'USER', payload: { name, age } });
}
it('provides values for effects inside arrays', () => {
return expectSaga(saga)
.provide([
[select(selectors.getName), 'Tucker'],
[select(selectors.getAge), 11],
])
.put({
type: 'USER',
payload: { name: 'Tucker', age: 11 },
})
.run();
});
Providing in Forked/Spawned Sagas
Providers work for effects in forked/spawned sagas too.
import { call, fork, put } from 'redux-saga/effects';
import { expectSaga } from 'redux-saga-test-plan';
import * as matchers from 'redux-saga-test-plan/matchers';
import api from 'my-api';
function* fetchUserSaga() {
const user = yield call(api.fetchUser);
yield put({ type: 'RECEIVE_USER', payload: user });
}
function* forkingSaga() {
yield fork(fetchUserSaga);
}
function* spawningSaga() {
yield spawn(fetchUserSaga);
}
it('provides values in forked sagas', () => {
return expectSaga(forkingSaga)
.provide([
[matchers.call.fn(api.fetchUser), fakeUser],
])
.put({ type: 'RECEIVE_USER', payload: fakeUser })
.run();
});
it('provides values in spawned sagas', () => {
return expectSaga(spawningSaga)
.provide([
[matchers.call.fn(api.fetchUser), fakeUser],
])
.put({ type: 'RECEIVE_USER', payload: fakeUser })
.run();
});
More Examples
For some more contrived examples of providers, look in the repo tests.
Caveats
For providers to work, expectSaga
will necessarily wrap forked/spawned sagas
with an intermediary generator called sagaWrapper
in order to intercept
effects. To ensure that your saga receives back a task object with a correct
name
property, Redux Saga Test Plan will attempt to rename the sagaWrapper
function to the name of a forked saga. This works in almost all JavaScript
environments but will fail in PhantomJS. Therefore, you can't depend on the
task name
property being correct in PhantomJS.