Skip to content

Commit

Permalink
Add redux unit tests
Browse files Browse the repository at this point in the history
Tests for the two ducks' reducers and the patreon epic.
  • Loading branch information
brettdh committed Jan 16, 2018
1 parent 70c35f2 commit d0d2508
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 18 deletions.
6 changes: 5 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
{
"extends": "airbnb",
"env": {
"jest": true
},
"rules": {
// Apprently react-native doesn't like .jsx files
"react/jsx-filename-extension": ["error", { "extensions": [".js"] }],
"import/prefer-default-export": "off"
"import/prefer-default-export": "off",
"import/no-extraneous-dependencies": ["error", { devDependencies: true }]
}
}
14 changes: 0 additions & 14 deletions __tests__/.eslintrc

This file was deleted.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"rxjs": "^5.5.6"
},
"devDependencies": {
"axios-mock-adapter": "^1.10.0",
"babel-jest": "21.2.0",
"babel-preset-react-native": "4.0.0",
"eslint": "^4.9.0",
Expand All @@ -36,7 +37,10 @@
"react-test-renderer": "16.0.0-beta.5"
},
"jest": {
"preset": "react-native"
"preset": "react-native",
"transformIgnorePatterns": [
"node_modules/(?!(jest-)?react-native|react-navigation)"
]
},
"comment": "flow is disabled in pre-commit because react-navigation breaks it",
"pre-commit": [
Expand Down
3 changes: 3 additions & 0 deletions src/state/ducks/auth/selectors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function isAuthenticated(state) {
return state.auth.authenticated;
}
26 changes: 26 additions & 0 deletions src/state/ducks/auth/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import configureStore from '../../store';
import * as actions from './actions';
import * as selectors from './selectors';

let store;

beforeEach(() => {
store = configureStore({ noEpic: true });
});

test('user is initially not logged in', () => {
expect(selectors.isAuthenticated(store.getState())).toBe(false);
});

test('login() logs the user in', () => {
store.dispatch(actions.login());
expect(selectors.isAuthenticated(store.getState())).toBe(true);
});

test('logout() logs the user out', () => {
store.dispatch(actions.login());
expect(selectors.isAuthenticated(store.getState())).toBe(true);

store.dispatch(actions.logout());
expect(selectors.isAuthenticated(store.getState())).toBe(false);
});
112 changes: 112 additions & 0 deletions src/state/ducks/patreon/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { ActionsObservable } from 'redux-observable';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';

import configureStore from '../../store';
import * as types from './types';
import * as actions from './actions';
import * as selectors from './selectors';
import epic from './epic';

/*
* Reducers are tested by stubbing out the epic with one that does nothing,
* and then asserting things about the synchronous actions only. Later,
* we test that the epics emit those synchronous actions in response
* to the async stuff they do, which closes the loop and effectively
* tests that dispatching the async action that the epic handled
* eventually caused the expected state change.
*
* This is a lot simpler than trying to test the entire store
* with the redux-observable middleware installed, and it gives us
* the same coverage.
*/

describe('patreon reducer', () => {
let store;

beforeEach(() => {
store = configureStore({ noEpic: true });
});

test('patreon is initially not enabled', () => {
expect(selectors.isPatron(store.getState())).toBe(false);
});

test('patreonEnabled() enables patreon', () => {
store.dispatch(actions.patreonEnabled());
expect(selectors.isPatron(store.getState())).toBe(true);
});

test('patreonDisabled() disables patreon', () => {
store.dispatch(actions.patreonEnabled());
expect(selectors.isPatron(store.getState())).toBe(true);

store.dispatch(actions.patreonDisabled());
expect(selectors.isPatron(store.getState())).toBe(false);
});
});


/*
* Direct epic tests are very simple. An epic is just a function
* that accepts an ActionsObservable and returns an Observable.
* A test for an epic therefore just has to:
* 1) Create an ActionObservable that emits the action(s) you want to test
* 2) Assert that the Observable the epic returns emits the expected action(s)
*/

describe('patreon epic', () => {
const baseUrl = 'https://httpbin.org';
let mock;

beforeEach(() => {
mock = new MockAdapter(axios);
});

// These tests are very simple; one action leads to a single other.
// Other tests that result in multiple actions from the epic
// will have to be a little more complicated.

describe('when Patreon API call succeeds', () => {
beforeEach(() => {
mock.onPost(new RegExp(`${baseUrl}/.*`)).reply(200, {});
});

test('enable() leads to patreonEnabled()', () => {
const action$ = ActionsObservable.of(actions.enable());
return expect(epic(action$).toPromise()).resolves.toEqual(actions.patreonEnabled());
});

test('disable() leads to patreonDisabled()', () => {
const action$ = ActionsObservable.of(actions.disable());
return expect(epic(action$).toPromise()).resolves.toEqual(actions.patreonDisabled());
});
});

describe('when Patreon API call fails', () => {
const status = 429;

beforeEach(() => {
mock.onPost(new RegExp(`${baseUrl}/.*`)).reply(status, {});
});

function expectErrorActionWithStatus(action, statusCode) {
expect(action.type).toEqual(types.PATREON_ERROR);
expect(action.error).toBe(true);
expect(action.payload).toBeInstanceOf(Error);
expect(action.payload.response.status).toEqual(statusCode);
}

test('enable() leads to patreonError()', async () => {
const action$ = ActionsObservable.of(actions.enable());
const errorAction = await epic(action$).toPromise();
expectErrorActionWithStatus(errorAction, status);
});

test('disable() leads to patreonError()', async () => {
const action$ = ActionsObservable.of(actions.disable());
const errorAction = await epic(action$).toPromise();
expectErrorActionWithStatus(errorAction, status);
});
});
});
18 changes: 16 additions & 2 deletions src/state/store.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
import { combineReducers, createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import { combineEpics, createEpicMiddleware } from 'redux-observable';
import { Observable } from 'rxjs';
import 'rxjs/add/observable/never';

import * as reducers from './ducks';
import epics from './epics';

export default function configureStore() {
/**
* A fake epic that just swallows actions. Useful for synchronous unit tests.
*/
const emptyEpic = () => Observable.never();

/**
* Return a redux store configured with all the reducers and middleware.
*
* If noEpic is passed, use emptyEpic to swallow all async actions.
* This allows testing the store with just synchronous actions,
* not worrying about side effects.
*/
export default function configureStore({ noEpic = false }) {
const reducer = combineReducers(reducers);
const epic = combineEpics(...epics);
const epic = noEpic ? combineEpics(...epics) : emptyEpic;
const epicMiddleware = createEpicMiddleware(epic);
const enhancer = composeWithDevTools(applyMiddleware(epicMiddleware));
return createStore(reducer, enhancer);
Expand Down
10 changes: 10 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,12 @@ aws4@^1.2.1, aws4@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"

axios-mock-adapter@^1.10.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/axios-mock-adapter/-/axios-mock-adapter-1.10.0.tgz#3ccee65466439a2c7567e932798fc0377d39209d"
dependencies:
deep-equal "^1.0.1"

axios@^0.17.1:
version "0.17.1"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.17.1.tgz#2d8e3e5d0bdbd7327f91bc814f5c57660f81824d"
Expand Down Expand Up @@ -1369,6 +1375,10 @@ decamelize@^1.0.0, decamelize@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"

deep-equal@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"

deep-extend@~0.4.0:
version "0.4.2"
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f"
Expand Down

0 comments on commit d0d2508

Please sign in to comment.