diff --git a/docs/recipes/WritingTests.md b/docs/recipes/WritingTests.md index 1bf0d7b5f9..5de5c9b6ca 100644 --- a/docs/recipes/WritingTests.md +++ b/docs/recipes/WritingTests.md @@ -60,7 +60,9 @@ describe('actions', () => { }); ``` -If you write async action creators +### Async Action Creators + +For async action creators using [Redux Thunk](https://github.com/gaearon/redux-thunk) or other middleware, it’s best to completely mock the Redux store for tests. You can still use [`applyMiddleware()`](../api/applyMiddleware.md) with a mock store, as shown below. You can also use [nock](https://github.com/pgte/nock) to mock the HTTP requests. ```js function fetchTodosRequest() { @@ -98,31 +100,67 @@ can be tested like: ```js import expect from 'expect'; -import * as actions from '../../actions/TodoActions'; +import { applyMiddleware } from 'redux'; +import thunk from 'redux-thunk'; +import * as actions from '../../actions/counter'; import * as types from '../../constants/ActionTypes'; import nock from 'nock'; +const middlewares = [thunk]; + +/** + * Creates a mock of Redux store with middleware. + */ +function mockStore(getState, expectedActions, onLastAction) { + if (!Array.isArray(expectedActions)) { + throw new Error('expectedActions should be an array of expected actions.'); + } + if (typeof onLastAction !== 'undefined' && typeof onLastAction !== 'function') { + throw new Error('onLastAction should either be undefined or function.'); + } + + function mockStoreWithoutMiddleware() { + return { + getState() { + return typeof getState === 'function' ? + getState() : + getState; + }, + + dispatch(action) { + const expectedAction = expectedActions.shift(); + expect(action).toEqual(expectedAction); + if (onLastAction && !expectedActions.length) { + onLastAction(); + } + return action; + } + } + } + + const mockStoreWithMiddleware = applyMiddleware( + ...middlewares + )(mockStoreWithoutMiddleware); + + return mockStoreWithMiddleware(); +} + describe('async actions', () => { - afterEach(() => nock.cleanAll() ); + afterEach(() => { + nock.cleanAll(); + }); - it('creates FETCH_TODO_SUCCESS when fechting todos has been done', (done) => { + it('creates FETCH_TODO_SUCCESS when fetching todos has been done', (done) => { nock('http://example.com/') .get('/todos') .reply(200, { todos: ['do something'] }); - let expectedActions = [ + const expectedActions = [ { type: types.FETCH_TODO_REQUEST }, { type: types.FETCH_TODO_SUCCESS, body: { todos: ['do something'] } } ] - - function mockDispatch(action) { - var expectedAction = expectedActions.shift(); - expect(action).toEqual(expectedAction); - if (!expectedActions.length) { - done(); - } - } - actions.fetchTodos()(mockDispatch); + const store = mockStore({ todos: [] }, expectedActions, done); + store.dispatch(actions.fetchTodos()); }); }); ``` diff --git a/examples/counter/test/actions/counter.spec.js b/examples/counter/test/actions/counter.spec.js index 60fdce164a..477e70d93d 100644 --- a/examples/counter/test/actions/counter.spec.js +++ b/examples/counter/test/actions/counter.spec.js @@ -1,6 +1,47 @@ import expect from 'expect'; +import { applyMiddleware } from 'redux'; +import thunk from 'redux-thunk'; import * as actions from '../../actions/counter'; +const middlewares = [thunk]; + +/* + * Creates a mock of Redux store with middleware. + */ +function mockStore(getState, expectedActions, onLastAction) { + if (!Array.isArray(expectedActions)) { + throw new Error('expectedActions should be an array of expected actions.'); + } + if (typeof onLastAction !== 'undefined' && typeof onLastAction !== 'function') { + throw new Error('onLastAction should either be undefined or function.'); + } + + function mockStoreWithoutMiddleware() { + return { + getState() { + return typeof getState === 'function' ? + getState() : + getState; + }, + + dispatch(action) { + const expectedAction = expectedActions.shift(); + expect(action).toEqual(expectedAction); + if (onLastAction && !expectedActions.length) { + onLastAction(); + } + return action; + } + }; + } + + const mockStoreWithMiddleware = applyMiddleware( + ...middlewares + )(mockStoreWithoutMiddleware); + + return mockStoreWithMiddleware(); +} + describe('actions', () => { it('increment should create increment action', () => { expect(actions.increment()).toEqual({ type: actions.INCREMENT_COUNTER }); @@ -10,32 +51,26 @@ describe('actions', () => { expect(actions.decrement()).toEqual({ type: actions.DECREMENT_COUNTER }); }); - it('incrementIfOdd should create increment action', () => { - const fn = actions.incrementIfOdd(); - expect(fn).toBeA('function'); - const dispatch = expect.createSpy(); - const getState = () => ({ counter: 1 }); - fn(dispatch, getState); - expect(dispatch).toHaveBeenCalledWith({ type: actions.INCREMENT_COUNTER }); + it('incrementIfOdd should create increment action', (done) => { + const expectedActions = [ + { type: actions.INCREMENT_COUNTER } + ]; + const store = mockStore({ counter: 1 }, expectedActions, done); + store.dispatch(actions.incrementIfOdd()); }); - it('incrementIfOdd shouldnt create increment action if counter is even', () => { - const fn = actions.incrementIfOdd(); - const dispatch = expect.createSpy(); - const getState = () => ({ counter: 2 }); - fn(dispatch, getState); - expect(dispatch.calls.length).toBe(0); + it('incrementIfOdd shouldnt create increment action if counter is even', (done) => { + const expectedActions = []; + const store = mockStore({ counter: 2 }, expectedActions); + store.dispatch(actions.incrementIfOdd()); + done(); }); - // There's no nice way to test this at the moment... - it('incrementAsync', (done) => { - const fn = actions.incrementAsync(1); - expect(fn).toBeA('function'); - const dispatch = expect.createSpy(); - fn(dispatch); - setTimeout(() => { - expect(dispatch).toHaveBeenCalledWith({ type: actions.INCREMENT_COUNTER }); - done(); - }, 5); + it('incrementAsync should create increment action', (done) => { + const expectedActions = [ + { type: actions.INCREMENT_COUNTER } + ]; + const store = mockStore({ counter: 0 }, expectedActions, done); + store.dispatch(actions.incrementAsync(100)); }); }); diff --git a/examples/real-world/createDevToolsWindow.js b/examples/real-world/createDevToolsWindow.js index e2c64f81cf..97c26b562c 100644 --- a/examples/real-world/createDevToolsWindow.js +++ b/examples/real-world/createDevToolsWindow.js @@ -16,7 +16,7 @@ export default function createDevToolsWindow(store) { ); if (!win) { - console.error( + console.error( // eslint-disable-line no-console 'Couldn\'t open Redux DevTools due to a popup blocker. ' + 'Please disable the popup blocker for the current page.' );