Redux-test-belt is a JavaScript testing utility for Redux that makes it easier to assert, isolate, manipulate, and traverse your store's output.
Redux-test-belt's API is meant to be flexible by mimicking / extending Redux's store functionality. Furthermore, Redux-test-belt is unopinionated regarding which test runner, assertion library or how your application is constructed.
To get started with Redux-test-belt, you can simply install it with NPM:
npm install --save-dev redux-test-belt
Redux-test-belt is currently compatible with all versions of Redux.
Redux-test-belt uses store enhanchers in order to improve the testing mechanisms. Getting started is a simple as:
import initStore from 'redux-test-belt';
const mockStore = initStore();
const store = mockStore({});
store.dispatch({type: 'START'});
store.dispatch({type: 'START'});
console.assert(store.getActions().length === 2, ">> Error")
Redux-test-belt takes seconds to setup against your testing suite. The following example uses Ava as a test runner:
import initStore from 'redux-test-belt';
import { describe } from 'ava-spec';
import thunk from 'redux-thunk';
describe('e2e test', it => {
it.beforeEach(t => {
// Before store's creation, we need to bootstrap redux-test-belt
// Any additional middlewares may be passed along store's creation
t.context.mockStoreWithMiddleware = initStore([thunk]);
});
it.afterEach(t => {
delete t.context.mockStoreWithMiddleware;
});
it('dispatches increment action', t => {
// setup
const store = t.context.mockStoreWithMiddleware({});
const action = { type: 'INCREMENT' };
store.dispatch(action);
t.deepEqual(store.getActions(), [action]);
});
});
initStore(middlewares?: Object) => mockStoreWrapper
middlewares
(Array of middlewares
[optional])
initStore()
is a single entry point bootstrapping Redux-test-belt's store. It accepts any additional middlewares that
may be required by each test. initStore exposes a single constructor function.
// Usage without middlewares
import initStore from '../src/modules/mockStore';
const storeInit = initStore();
// Usage with middlewares
import initStore from 'redux-test-belt';
import thunk from 'redux-thunk';
import promise from 'redux-promise';
const storeInit = initStore([thunk, promise]);
mockStore(initialState = null : Object,
reducers = (currentState, action) => currentState: Function ,
blockCheck = (...args) => true : Function) => Store
initialState
(any
[optional]) The initial state of the storereducers
(Function
[optional]) A reducing function that returns the next state tree, given the current state tree and an action to handle.combineReducers
may be used in order to invoke multiple reducers.blockCheck
(Function
[optional]) .blockCheck
is a predicate function used along with theblockMiddleware
. It is primarily used in order to limit down the range of the actions invoked during our application's testing.
mockStore()
is an extended version of Redux's [createStore()](http://redux.js.org/docs/api/createStore.html)
. Using store enhancers it adds additional testing utilities combining separated middlewares. This is mainly the heart of each testing unit.
// use mockStore with initial state
import initStore from 'redux-test-belt';
import test from 'ava';
test(t => {
const storeInit = initStore();
const store = t.context.mockStore({ state: 1, items:[] });
});
// use mockStore with initial state and a reducer
import initStore from 'redux-test-belt';
import test from 'ava';
test(t => {
const storeInit = initStore();
// reducer
function reducer(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return state.concat([action.payload]);
default:
return state;
}
};
const store = t.context.mockStore({}, reducer);
});
// use mockStore along with combineReducers
import initStore from 'redux-test-belt';
import { combineReducers } from 'redux';
import test from 'ava';
test(t => {
const storeInit = initStore();
// reducer
function firstReducer(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return state.concat([action.payload]);
default:
return state;
}
};
function secondReducer(state = [], action) {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state
}
};
const reducers = combineReducers({
firstReducer,
secondReducer
});
const store = t.context.mockStore({}, reducers);
});
// use mockStore with initial state, a reducer and a block checking predicate function
import initStore from 'redux-test-belt';
import test from 'ava';
test(t => {
const storeInit = initStore();
// reducer
function reducer(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return state.concat([action.payload]);
default:
return state;
}
};
function()
const store = t.context.mockStore({}, reducer);
});
mockStore()
returns a Redux store instance extended by several testing utilities. Most of the packed functionality is separated in isolated middlewares as well, providing a clean and powerful testing workflow.
store.dispatch(action?: Object)
action
(Object
) A plain object describing the change that makes sense for your application. Actions are the only way to get data into the store. As a result, any kind of data indicating a store change, whether from UI events, network callbacks, or other sources such as WebSockets needs to eventually be dispatched as actions.
dispatch()
is used to dispatch actions to the store. This is the only way to trigger a state change. The functionality provided from this method is identical to Redux's dispatch().
// use mockStore with initial state
import initStore from 'redux-test-belt';
import test from 'ava';
test(t => {
const storeInit = initStore();
const store = t.context.mockStore({});
store.dispatch({ type: 'INCREMENT' });
});
store.getState() => state
getState()
returns the current state tree of your application.
It is equal to the last value returned by the store’s reducer. The functionality provided from this method is identical to Redux's getState().
In most cases testing a Redux application includes testing against the state of the store. In the other side, unit test may require checking the actions dispatched. Redux-test-belt comes with a set of helper functions providing an ease applying assertions on the action chain.
store.getActions() => [Object]
store.getActions()
returns a list with all the actions dispatched against the store. Keep in mind, that the actions dispatched may be asynchronous, store.getActions()
keeps track on the actions based on time on when the actions are fired through the action creators.
import initStore from 'redux-test-belt';
import test from 'ava';
test(t => {
const storeInit = initStore();
const store = t.context.mockStore({});
const actions = [
{ type: 'BAR' },
{ type: 'foo', payload: 'PAYLODED' }
];
store.dispatch(actions[0]);
store.dispatch(actions[1]);
t.deepEqual(store.getActions(), actions);
});
store.hasActions(matcher? Object(s) || String(s) ) => Boolean
matcher
(Object(s) || String(s)
) Using matcher you may filter down the actions dispatched on the store. A matcher may be one or more action objects deeply matching the action queue or even just the action type(s).
In large scale applications testing against the actions dispatched may be tedious. Matching a list of actions usually requires an assertion library, like Chai and lots of configuration. Redux-test-belt is packed with the store.hasActions()
as utility helper.
import initStore from 'redux-test-belt';
import test from 'ava';
test(t => {
const storeInit = initStore();
const store = t.context.mockStore({});
store.dispatch({ type: 'ACTION_1' });
store.dispatch({ type: 'ACTION_2', payload: 'PAYLODED' });
store.dispatch({ type: 'ACTION_3' });
// Τesting against a superset of action objects
t.true(store.hasActions(
{ type: 'ACTION_1' },
{ type: 'ACTION_2', payload: 'PAYLODED' }));
t.true(store.hasActions(
{ type: 'ACTION_3' },
{ type: 'ACTION_1' }));
});
import initStore from 'redux-test-belt';
import test from 'ava';
test(t => {
const storeInit = initStore();
const store = t.context.mockStore({});
store.dispatch({ type: 'ACTION_1' });
store.dispatch({ type: 'ACTION_2', payload: 'PAYLODED' });
store.dispatch({ type: 'ACTION_3' });
// Τesting against a superset of action types
t.true(store.hasActions('ACTION_1', 'ACTION_2'));
t.true(store.hasActions('ACTION_3', 'ACTION_2'));
});
store.clearActions() => []
To help a test suite DRY up any duplicated setup and teardown code, Redux-test-belt provides the store.clearActions()
method. In particular, store.clearActions()
clears up the actions recorded during the store's lifecycle. It may be used on global before & after hooks, in order to keep each test isolated and independent.
import initStore from 'redux-test-belt';
import test from 'ava';
test(t => {
const storeInit = initStore();
const store = t.context.mockStore({});
store.dispatch({ type: 'ACTION_1' });
store.dispatch({ type: 'ACTION_2' });
t.true(store.hasActions('ACTION1', 'ACTION2'));
store.clearActions();
t.deepEqual(store.getActions(), []);
});
import initStore from 'redux-test-belt';
import test from 'ava';
test.beforeEach(t => {
// Mock the store before each test
t.context.storeInit = initStore();
t.context.store = t.context.mockStore({});
});
test.afterEach(t => {
// Clear all the actions after each test
t.context.store.clearActions();
});
test(t => {
t.context.dispatch({ type: 'ACTION_1' });
t.context.dispatch({ type: 'ACTION_2' });
t.true(t.context.hasActions('ACTION1', 'ACTION2'));
});
Redux applications may use asynchronous action creators; testing asynchronous actions that will be processed by reducers synchronously it's hard to deal with, though. Redux-test-belt provides async helper methods in order to write serialized tests. Keep in mind that async action handling requires using a thunk middleware for Redux like redux-thunk.
store.getPromises() => [Object]
store.getPromises()
grabs all the actions dispatched on the store that are encapsulated as Promise objects.
import initStore from 'redux-test-belt';
import thunk from 'redux-thunk';
import test from 'ava';
test.serial(t => {
function superAction() {
return {
type: 'ACTION_DISPATCHED'
};
}
function asynchronousAction() {
return dispatch => {
return Promise.resolve()
.then(() => dispatch(superAction()));
};
}
const storeInit = initStore([thunk]);
const store = t.context.mockStore({});
return store.dispatch(asynchronousAction())
.then(() => {
t.is(store.getPromises().length, 1);
});
});
store.getPending() => [Object]
store.getPending()
returns all the actions dispatched on the store that are encapsulated as Promises and their initial state is not fulfilled, nor rejected yet.
import initStore from 'redux-test-belt';
import thunk from 'redux-thunk';
import test from 'ava';
test(t => {
function superAction() {
return {
type: 'ACTION_DISPATCHED'
};
}
function asynchronousAction() {
return dispatch => {
return Promise.all([
dispatch(superAction())
])
};
}
const storeInit = initStore([thunk]);
const store = t.context.mockStore({});
// the middleware messes the actions clear them :)
store.dispatch(asynchronousAction())
t.is(store.getPending().length, 1);
});
store.getRejected() => [Object]
store.getRejected()
returns all the actions dispatched on the store that are encapsulated as Promises and yet settled.
import initStore from 'redux-test-belt';
import thunk from 'redux-thunk';
import test from 'ava';
test.serial(t => {
function superAction() {
return {
type: 'ACTION_DISPATCHED'
};
}
function asynchronousAction() {
return dispatch => {
return Promise.reject()
.then(() => dispatch(superAction()));
};
}
const store = t.context.mockStore({});
// the middleware messes the actions clear them :)
return store.dispatch(asynchronousAction())
.then(() => {
t.is(store.getPending().length, 0);
t.is(store.getRejected().length, 1);
t.is(store.getPromises().length, 1);
});
});
store.getResolved() => [Object]
store.getResolved()
returns all the actions dispatched on the store that are encapsulated as Promises and settled, or locked into a promise chain.
import initStore from 'redux-test-belt';
import thunk from 'redux-thunk';
import test from 'ava';
test.serial(t => {
function superAction() {
return {
type: 'ACTION_DISPATCHED'
};
}
function asynchronousAction() {
return dispatch => {
return Promise.resolve()
.then(() => dispatch(superAction()));
};
}
const store = t.context.mockStore({});
// the middleware messes the actions clear them :)
return store.dispatch(asynchronousAction())
.then(() => {
t.is(store.getPending().length, 0);
t.is(store.getResolved().length, 1);
t.is(store.getPromises().length, 1);
});
});
store.clearPromises() => []
To help a test suite DRY up any duplicated setup and teardown code, Redux-test-belt provides the store.clearPromises()
method. In particular, store.clearPromises()
clears up the recorded actions that are encapsulated as Promise objects during store's lifecycle. It may used on global before & after hooks in order to keep each test isolated and independent .
import initStore from 'redux-test-belt';
import thunk from 'redux-thunk';
import test from 'ava';
test.serial(t => {
function superAction() {
return {
type: 'ACTION_DISPATCHED'
};
}
function asynchronousAction() {
return dispatch => {
return Promise.resolve()
.then(() => dispatch(superAction()));
};
}
const store = t.context.mockStore({});
// the middleware messes the actions clear them :)
return store.dispatch(asynchronousAction())
.then(() => {
t.is(store.getResolved().length, 1);
t.is(store.getPromises().length, 1);
store.clearPromises();
t.is(store.getResolved().length, 0);
t.is(store.getPromises().length, 0);
});
});
In complex applications where lots of actions take place testing might be bloated. Redux-test-belt provides a set of utilities which enable users to filter the actions dispatched, clean up any redundant actions as well as to prevent state's manipulation. Each blocked action is stored for later use and may be accessed on demand later. All the blocked actions are actually dispatched on the store in order to keep Redux in place, the action type is marked as âś‹BLOCKED_ACTION
whilst the payload holds the very first action created.
As it was mentioned earlier, you may pass a predicate function as a parameter during mockStore's instantiation. Blocking prediction may take place, either or the state or the action that takes place.
import initStore from 'redux-test-belt';
const mockStore = initStore([]);
const reducer = (currentState, action) => currentState;
const blockCheck = (currentState, action) => {
return (action.type === 'BLOCKED') ? false : true;
}
const store = mockStore({},reducer, blockCheck);
store.dispatch({ type: 'BLOCKED' });
store.dispatch({ type: 'NON-BLOCKED' });
store.getBlocked() => [actions]
store.getBlocked()
returns all the blocked actions filtered from the passed predicate function.
import initStore from 'redux-test-belt';
import thunk from 'redux-thunk';
import test from 'ava';
test.serial(t => {
const mockStore = initStore([]);
const reducer = (currentState, action) => currentState;
const blockCheck = (currentState, action) => {
return (action.type === 'BLOCKED') ? false : true;
}
const store = mockStore({},reducer, blockCheck);
store.dispatch({ type: 'BLOCKED' });
store.dispatch({ type: 'NON-BLOCKED' });
t.deepEqual(store.getBlocked(),[{ type: 'BLOCKED' }]);
});
store.clearBlocked() => []
store.clearBlocked()
clears all the blocked actions stored.
import initStore from 'redux-test-belt';
import thunk from 'redux-thunk';
import test from 'ava';
test(t => {
const mockStore = initStore([]);
const reducer = (currentState, action) => currentState;
const blockCheck = (currentState, action) => false;
const store = mockStore({},reducer, blockCheck);
store.dispatch({ type: 'BLOCKED' });
t.deepEqual(store.getBlocked(),[{ type: 'BLOCKED' }]);
store.clearBlocked();
t.deepEqual(store.getBlocked(),[]);
});
store.hasBlocked(matcher? Object(s) || String(s) ) => Boolean
matcher
(Object(s) || String(s)
) Using matcher you may filter down the repository of blocked actions which is being retained inside the store's instance. A matcher may be one or more action objects deeply matching the blocked action queue or even just the action type(s).
import initStore from 'redux-test-belt';
import thunk from 'redux-thunk';
import test from 'ava';
test(t => {
const mockStore = initStore([]);
const reducer = (currentState, action) => currentState;
const blockCheck = (currentState, action) => false;
const store = mockStore({},reducer, blockCheck);
store.dispatch({ type: 'ACTION1' });
store.dispatch({ type: 'ACTION2', payload: 'PAYLODED' });
t.true(store.hasBlocked('ACTION1', 'ACTION2'));
store.dispatch({ type: 'ACTION3' });
t.true(store.hasBlocked('ACTION1', 'ACTION2', 'ACTION3'));
t.false(store.hasBlocked('whatever', 'ACTION1'));
});
Redux-test-belt declares orphans as actions that don't differentiate or manipulate the actual state. Where lots of action are dispatched simultaneously this feature allows testing to narrow down the results, take action on dead code and improve debugging.
store.getOrphans() => [Object]
store.getOrphans()
returns a list of actions that didn't change the state upon dispatching.
import initStore from 'redux-test-belt';
import thunk from 'redux-thunk';
import test from 'ava';
test(t => {
function fakeDispather() {
return {
type: 'ADD_TODO'
}
}
const mockStore = initStore([]);
const store = mockStore({});
store.dispatch(fakeDispather());
store.dispatch(fakeDispather());
t.is(store.getOrphans().length, 2);
t.deepEqual(
store.getOrphans(),
[{ type: 'ADD_TODO' }, { type: 'ADD_TODO' }]);
});
store.hasActions(matcher? Object(s) || String(s) ) => Boolean
matcher
(Object(s) || String(s)
) Using matcher you may filter down the orphans' repository. A matcher may be one or more action objects deeply matching the action queue or even just the action type(s).
import initStore from 'redux-test-belt';
import thunk from 'redux-thunk';
import test from 'ava';
test(t => {
function fakeDispather() {
return {
type: 'ADD_TODO'
}
}
const mockStore = initStore([]);
const store = mockStore({});
store.dispatch(fakeDispather());
store.dispatch(fakeDispather());
t.is(store.hasOrphans('ADD_TODO'), true);
t.is(store.hasOrphans({type: 'ADD_TODO' }), true);
});
store.clearOrphans() => []
store.clearOrphans()
truncates the list of orphan actions.
import initStore from 'redux-test-belt';
import thunk from 'redux-thunk';
import test from 'ava';
test(t => {
function fakeDispather() {
return {
type: 'ADD_TODO'
}
}
const mockStore = initStore([]);
const store = mockStore({});
store.dispatch(fakeDispather());
store.dispatch(fakeDispather());
t.is(store.getOrphans().length, 2);
t.deepEqual(
store.getOrphans(),
[{ type: 'ADD_TODO' },{ type: 'ADD_TODO' }]);
store.clearOrphans();
t.is(store.getOrphans().length, 0);
});
Where default createStore() is used, it seems a bit redundant to write additional tests using Redux-test-belt's mockStore()
. Therefore, Redux-test-belt exports each module as a flexible middleware.
import { createStore } from 'redux';
import { blockMiddleware, blockedActions, PREFIX_ACTION } from 'redux-test-belt';
const blocking = (currentState, action) => false;
function reducer(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return state.concat([ action.payload ])
default:
return state
}
}
let store = createStore(reducer,
[ 'Use Redux' ],
applyMiddleware.apply(null, [blockMiddleware(blocking)])
);
store.dispatch({
type: 'ACTION',
text: 'Read the docs'
});
// Expose each method
console.log(blockedActions.getBlocked());
//Alias: blockedActions.get()
console.log(blockedActions.hasBlocked('ACTION'));
//Alias: blockedActions.has()
blockedActions.clearBlocked();
console.log(blockedActions.getBlocked());
console.log(PREFIX_ACTION);
import { createStore } from 'redux';
import { loggedActions, actionsLoggerMiddleware } from 'redux-test-belt';
function reducer(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return state.concat([ action.payload ])
default:
return state
}
}
let store = createStore(reducer,
[ 'Use Redux' ],
applyMiddleware.apply(null, [actionsLoggerMiddleware])
);
store.dispatch({
type: 'ACTION',
text: 'Read the docs'
});
// Expose each method
console.log(loggedActions.getActions());
// Alias: loggedActions.get()
console.log(loggedActions.hasActions('ACTION'));
// loggedActions.has()
loggedActions.clearActions();
console.log(loggedActions.getActions());
import { createStore } from 'redux';
import { orphansMiddleware, orphans } from 'redux-test-belt';
function reducer(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return state.concat([ action.payload ])
default:
return state
}
}
let store = createStore(reducer,
[ 'Use Redux' ],
applyMiddleware.apply(null, [orphansMiddleware])
);
store.dispatch({
type: 'ACTION',
text: 'Read the docs'
});
// Expose each method
console.log(orphans.getOrphans());
//Alias: orphans.get()
console.log(orphans.hasOrphans('ACTION'));
//Alias: orphans.has()
orphans.clearOrphans();
console.log(orphans.getOrphans());
import { createStore } from 'redux';
import { promiseMiddleware, promises } from 'redux-test-belt';
function reducer(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return state.concat([ action.payload ])
default:
return state
}
}
function superAction() {
return {
type: 'ACTION_DISPATCHED'
};
}
function asynchronousAction() {
return dispatch => {
return Promise.resolve()
.then(() => dispatch(superAction()));
};
}
let store = createStore(reducer,
[ 'Use Redux' ],
applyMiddleware.apply(null, [promiseMiddleware])
);
store.dispatch(asynchronousAction());
// Expose each method
console.log(promises.getPromises());
// Alias: promises.get();
console.log(promises.getPending());
console.log(promises.getRejected());
console.log(promises.getResolved());
promises.clear();
// Alias: promises.clearPromises();
console.log(promises.getPromises());