Dynamic Providers
You can use completely dynamic providers by passing in an object literal to the
provide
method. The object literal argument must contain effect creator names
as keys and function handlers as values. Each function handler takes two
arguments, the yielded effect and a next
callback. You can inspect the effect
and return a fake value based on the properties in the effect. If you don't want
to handle the effect yourself, you can pass it on to Redux Saga by invoking the
next
callback parameter.
Here is an example with Jest to show you how to supply a fake value for an API call:
import { call, put, take } from 'redux-saga/effects';
import { expectSaga } from 'redux-saga-test-plan';
import api from 'my-api';
function* saga() {
const action = yield take('REQUEST_USER');
const id = action.payload;
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({
call(effect, next) {
// Check for the API call to return fake value
if (effect.fn === api.fetchUser) {
const id = effect.args[0];
return { id, name: 'John Doe' };
}
// Allow Redux Saga to handle other `call` effects
return next();
},
})
.put({
type: 'RECEIVE_USER',
payload: { id: 1, name: 'John Doe' },
})
.dispatch({ type: 'REQUEST_USER', payload: 1 })
.run();
});
Effect Creators
Even though you'll probably never need them all, you can supply a provider for almost every effect creator*:
all
**actionChannel
call
cancel
cancelled
cps
flush
fork
getContext
join
put
race
select
setContext
spawn
take
*Because there is no way to distinguish the apply
and call
effects, you
must handle apply
effects with the call
provider function.
*To handle take.maybe
and put.resolve
, use the take
and put
providers,
respectively. You can inspect take
effects for the maybe
property and put
effects for the resolve
property.
** all
will provide values for a yielded all
effect as well as a yielded
array. NOTE: yielding an array is deprecated in Redux Saga, so this
functionality will be removed when Redux Saga removes support for yielded
arrays.
Other Examples
Select
import { put, select } from 'redux-saga/effects';
import { expectSaga } from 'redux-saga-test-plan';
import { selectors } from 'my-selectors';
function* saga() {
const name = yield select(selectors.getName);
const age = yield select(selectors.getAge);
yield put({ type: 'USER', payload: { name, age } });
}
it('provides a value for the selector', () => {
return expectSaga(saga)
.provide({
select({ selector }, next) {
if (selector === selectors.getName) {
return 'Tucker';
}
if (selector === selectors.getAge) {
return 11;
}
return next();
},
})
.put({
type: 'USER',
payload: { name: 'Tucker', age: 11 },
})
.run();
});
Parallel Effects via all
Providers work on effects yielded inside an all
effect.
import { all, 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({ selector }, next) {
if (selector === selectors.getName) {
return 'Tucker';
}
if (selector === selectors.getAge) {
return 11;
}
return next();
},
})
.put({
type: 'USER',
payload: { name: 'Tucker', age: 11 },
})
.run();
});
Or you can provide a value for the entire array of effects via the all
provider:
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 a value for the entire array', () => {
return expectSaga(saga)
.provide({
all: () => ['Tucker', 11],
})
.put({
type: 'USER',
payload: { name: 'Tucker', age: 11 },
})
.run();
});
Parallel Effects via an Array
Providers work on effects yielded inside an array. 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({ selector }, next) {
if (selector === selectors.getName) {
return 'Tucker';
}
if (selector === selectors.getAge) {
return 11;
}
return next();
},
})
.put({
type: 'USER',
payload: { name: 'Tucker', age: 11 },
})
.run();
});
Or you can provide a value for the entire array of effects via the all
provider:
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 a value for the entire array', () => {
return expectSaga(saga)
.provide({
all: () => ['Tucker', 11],
})
.put({
type: 'USER',
payload: { name: 'Tucker', age: 11 },
})
.run();
});
Race
import { call, put, race, take } from 'redux-saga/effects';
import { expectSaga } from 'redux-saga-test-plan';
import api from 'my-api';
const delay = time => (
new Promise(resolve => setTimeout(resolve, time))
);
function* saga() {
const action = yield take('REQUEST_USER');
const id = action.payload;
const { user } = yield race({
user: call(api.fetchUser, id),
timeout: call(delay, 500),
});
if (user) {
yield put({ type: 'RECEIVE_USER', payload: user });
} else {
yield put({ type: 'TIMEOUT' });
}
}
test('fetching the user succeeds', () => {
const fakeUser = { id: 1, name: 'John Doe' };
return expectSaga(saga)
.provide({
race: () => ({ user: fakeUser }),
})
.put({ type: 'RECEIVE_USER', payload: fakeUser })
.dispatch({ type: 'REQUEST_USER' })
.run();
});
test('timeout wins', () => {
return expectSaga(saga)
.provide({
race: () => ({ timeout: undefined }),
})
.put({ type: 'TIMEOUT' })
.dispatch({ type: 'REQUEST_USER' })
.run();
});
Errors
You can simulate errors by throwing from a provider:
function* saga() {
const action = yield take('REQUEST_USER');
const id = action.payload;
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('can throw errors in the saga', () => {
const error = new Error('Whoops...');
return expectSaga(saga)
.provide({
call(effect, next) {
if (effect.fn === api.fetchUser) {
throw error;
}
return next();
},
})
.put({ error, type: 'FAIL_USER' })
.dispatch({ type: 'REQUEST_USER', payload: 1 })
.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 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({
call({ fn }, next) {
if (fn === api.fetchUser) {
return fakeUser;
}
return next();
},
})
.put({ type: 'RECEIVE_USER', payload: fakeUser })
.run();
});
it('provides values in spawned sagas', () => {
return expectSaga(spawningSaga)
.provide({
call({ fn }, next) {
if (fn === api.fetchUser) {
return fakeUser;
}
return next();
},
})
.put({ type: 'RECEIVE_USER', payload: fakeUser })
.run();
});
Multiple Providers
You can supply multiple object providers via a couple methods. The easiest way
is to pass in an array of object providers to the provide
method. Provider
functions will be composed according to the effect type, meaning the provider
functions in the first object will be called before subsequent provider
functions in the array.
Because provider functions are composed, they are similar to middleware. The
next
function argument inside provider functions allows you to delegate to the
next provider in the middleware stack. If no more providers are available, then
next
will delegate to Redux Saga to handle the effect as normal.
import { call, put, select } from 'redux-saga/effects';
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' };
const provideUser = ({ fn, args: [id] }, next) => (
fn === api.findUser ? fakeUser : next()
);
const provideDog = ({ fn }, next) => (
fn === api.findDog ? fakeDog : next()
);
const provideOtherData = ({ selector }, next) => (
selector === selectors.getOtherData ? fakeOtherData : next()
);
it('takes multiple providers and composes them', () => {
return expectSaga(saga)
.provide([
{ call: provideUser, select: provideOtherData },
{ call: provideDog },
])
.put({
type: 'DONE',
payload: {
user: fakeUser,
dog: fakeDog,
greeting: 'hello',
otherData: fakeOtherData,
},
})
.run();
});
An alternative to supplying multiple provider objects is to only pass one object
into provide
and use the composeProviders
function to compose multiple
provider functions for a specific effect. You can import the composeProviders
function from the redux-saga-test-plan/providers
module. The provider
functions are composed from left to right.
import { composeProviders } from 'redux-saga-test-plan/providers';
it('takes multiple providers and composes them', () => {
return expectSaga(saga)
.provide({
call: composeProviders(
provideUser,
provideDog
),
select: provideOtherData,
})
.put({
type: 'DONE',
payload: {
user: fakeUser,
dog: fakeDog,
greeting: 'hello',
otherData: fakeOtherData,
},
})
.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.