21
Redux Testing Lessons Learned: Render Components with a Real Store
This post is a follow up to Redux Testing: Hard Lessons Learned where I spoke about two important principles for testing redux apps: "Stop Testing your Disconnected Components" and "Build a Small Utility Library". Mostly we went through some of the key utilities that I use to make testing redux apps more manageable. This post will cover another area I called out in the first post: Render Components with a Real Store
For a long time I relied on redux mock store to preload data into my components for rendering during tests. This approach makes it easy to take a snapshot of a component with arbitrary data and ensure that it renders correctly. Where it completely fails is testing interactions.
What happens when I click the close the button or when I select that image? With redux mock store you have a special method named getActions that tells you what actions were fired, but that's it. Those actions don't actually make it to your reducers and they never update the UI. This makes your tests pretty frustrating to write. There's no good way to confirm that a component can transition from one state to another. You can only test snapshots.
The first and quickest way to solve this is to pass your actual redux store into the <Provider>
you use to wrap your tests and then return it. For example:
import { render } from "@testing-library/react";
import { store } from "../app/store";
function renderWithContext(element) {
render(
<Provider store={store}>{element}</Provider>
);
return { store };
}
This immediately gives you all kinds of powers. The first one is the ability to dispatch actions to populate or otherwise your modify redux store. Because those actions are dispatched synchronously you can immediately assert that the UI was updated.
test("table should render all kinds of data", () => {
const { store } = renderWithContext(<ResultsTable />);
// expect() table to be empty
store.dispatch({ type: "POPULATE_DATA", data: { /* ... */ })
// expect() table to be full
});
The other thing it lets you do is assert that your redux store changed in response to an event that wouldn't normally affect the component you are testing. For example let's say you had a button that updated a counter, but that counter component lived somewhere else. We can pretty easily test that clicking the button updated the count in our store.
test("counter should update count", () => {
const { store } = renderWithContext(<CounterButton />);
expect(store.getState().count).toEqual(0);
userEvent.click(screen.getByRole("button"));
expect(store.getState().count).toEqual(1);
});
Now the issue with sharing your actual redux store is that the order of your tests shouldn't matter. You really want to run your tests in isolation. With the shared store approach if you dispatch an event in one test, the changes are propagated to all future tests. And that's why I ended up with the getStoreWithState
method I showed in my previous article as a key utility.
// ...
export const store = configureStore({ reducer });
export function getStoreWithState(preloadedState) {
return configureStore({ reducer, preloadedState });
}
There are two important parts here. The one that I mentioned before was the preloadedState
option, which lets us render components in tests with state already setup in a specific way (similar to mock redux store). The second and more subtle accomplishment here is that we're giving our generated store access to the same reducers used by our app's store. This gives us an isolated store to use for each test that also has access to the full power of our application's reducers.
One benefit of this approach is that every time we test a component hooked into redux, we're also testing multiple reducers. It more economical and it more accurately reflects how our application actually works. Not to mention your tests are way easier to write this way. If you're used to testing with mock-redux-store this approach is going to give you a huge boost.
If you want to learn more about my approach to testing redux applications please watch my course Confidently Testing Redux Applications with Jest and TypeScript.
21