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.
npm install --save-dev expect-redux
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
import { createStore, compose } from "redux";
export const configureStore = (extraStoreEnhancers = []) => {
const storeEnhancers = [
,
/* here goes e.g. applyMiddleware */ ...extraStoreEnhancers
];
const store = createStore(reducer, compose(...storeEnhancers));
return store;
};
With that, you can 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();
// ...
});
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
});
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.
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.
Matches by the passed type
of an action only
Matches an action equal to the passed object
(using R.equals
)
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
.
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.
Matches an action that both is of type type
and satisfies the given predicate
. Like above, predicate must be a function action => boolean
.
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.
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.
Will create an assertion that resolves once the store state matches the provided expected
state.
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");
- chai-redux - a very similar assertion library with a
chai
-like DSL and timing-based assertions (first this action, then that action) - redux-saga-test-plan - a testing framework similar to expect-redux, but tied closely to redux-saga
- redux-action-assertions
- redux-mock-store