expect-redux - better interaction testing for redux
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
dist
docs
examples
flow-typed/npm
src
test
.babelrc
.eslintrc
.flowconfig
.gitignore
.npmignore
.nvmrc
.travis.yml
LICENSE
README.md
package-lock.json
package.json
rollup.config.js

README.md

Logo

npm version CI Deps DevDeps

expect-redux - better interaction testing for redux

expect-redux is a testing library that enables you to write tests that verify the behavior of your business logic, no matter if you are using redux-saga, redux-observable or just redux-thunk. It provides a fluent DSL that makes writing tests with asynchronousness in mind a lot easier.

Here's a simple example to give you an idea:

it("should dispatch SUCCESSFULLY_CALLED on success", () => {
  const store = createStore(...);

  fetch('/some-call-to-an-api')
    .then(() => store.dispatch({ type: "SUCCESSFULLY_CALLED" }))

  return expectRedux(store)
    .toDispatchAnAction()
    .ofType("SUCCESSFULLY_CALLED")
});

It doesn't matter if the action is dispatched asynchronously or even if it already was dispatched when you call expectRedux(store)..., expect-redux records all previously dispatched actions as well as every action that will be dispatched.

See /examples for some example projects using different side-effect libraries that are tested with expect-redux, or checkout this blogpost where I compare testing strategies for several side-effect libraries and explain how expect-redux helps you to put them under test.

A first version of expect-redux was developed for use in our projects at @VaamoTech for Vaamo.

Installation

npm install --save-dev expect-redux

Usage

expect-redux asserts the behavior of your effects, so it's best if you test your store like you would create it in your application code, not in isolation.

In order to record actions that are dispatched to it, your store only needs to be configured with a spy as an additional storeEnhancer:

// store.js
export const configureStore = (extraStoreEnhancers = []) => {
  const storeEnhancers = [
    ,
    /* here goes e.g. applyMiddleware */ ...extraStoreEnhancers
  ];

  const store = createStore(reducer, compose(...storeEnhancers));

  return store;
};

With that, just add storeSpy from expect-redux as an extraStoreEnhancer in the setup of your tests:

import { configureStore } from "./store.js";
import { storeSpy } from "expect-redux";

const storeForTest = () => configureStore([storeSpy]);

describe("your test", () => {
  const store = storeForTest();
  // ...
});

API

expect-redux supports both test-runners that support waiting for a Promise to resolve, such as jest or mocha, but also (thanks to chai-redux for the inspiration) supplying a done callback to end(...) as the last call to every assertion.

it("works if you return a Promise", () => {
  return expectRedux(store)
    .toDispatchAnAction()
    .ofType("PROMISE");
});
it("works if you provide a `done` callback", done => {
  expectRedux(store)
    .toDispatchAnAction()
    .ofType("DONE")
    .end(done);
});

With the Promise-interface, it's easy to write async-await tests with assertions that wait for a certain action to be dispatched:

it("works if you use async-await", async () => {
  // do something...

  await expectRedux(store)
    .toDispatchAnAction()
    .ofType("WAITING FOR THIS");

  // do something else
});

Configuration

expectRedux.configure({ betterErrorMessagesTimeout: number | false })

Fail if no expectation matched after timeout miliseconds. This is a workaround so you get a meaningful error message instead of a timeout error. Can go into the setup file as it's a global switch.

A screencast showing how the error messages look like

Assertions on Actions

expect-redux is built to assert behavior of side-effects, less the state that results in an action being dispatched. toDispatchAnAction() and toNotDispatchAnAction() encourage testing the reducer in isolation and instead testing the action producing side-effects.

expectRedux(store).toDispatchAnAction().ofType(type)

Matches by the passed type of an action only

expectRedux(store).toDispatchAnAction().matching(object)

Matches an action equal to the passed object (using R.equals)

expectRedux(store).toDispatchAnAction().matching(predicate)

Matches an action that satisfies the given predicate. predicate must be a function Action => boolean, e.g. R.propEq('payload', 'foobar'). Will not fail if the predicate returns false.

expectRedux(store).toDispatchAnAction().asserting(assertion)

Matches an action that won't let the given assertion throw an exception. Assertion must be a function Action => any, e.g. action => expect(action.payload).toEqual(42). Will not fail if the assertion throws.

expectRedux(store).toDispatchAnAction().ofType(type).matching(predicate)

Matches an action that both is of type type and satisfies the given predicate. Like above, predicate must be a function action => boolean.

expectRedux(store).toDispatchAnAction().ofType(type).asserting(assertion)

Matches an action that both is of type type and does not let the given assertion throw. Assertion must be a function Action => any, e.g. action => expect(action.payload).toEqual(42). Will not fail if the assertion throws.

expectRedux(store).toNotDispatchAnAction(timeout: number)...

Will negate all following predicates. If an action is dispatched that matches the predicates before timeout was reached, the test will fail.

Note that this is a highly dangerous feature, as it relies on the timeout to prove that an action was not dispatched. If it takes longer for your side effect to dispatch the action, the test could misleadingly pass even though the action is ultimately dispatched to the store.

State related assertions

expectRedux(store).toHaveState().matching(expected: Object)

Will create an assertion that resolves once the store state matches the provided expected state.

expectRedux(store).toHaveState().withSubtree(selector: Object => mixed)...

Will select a subtree of the state before checking the following assertion. For example:

expectRedux(store)
  .toHaveState()
  .withSubtree(state => state.title)
  .matching("Welcome to my website");

Similar or related libraries