Hacking together a TDD workflow for redux and components
Whilst I typically spend more of my time working on backend code (in python) I also work on the frontend (mostly typescript). I’m a big fan of TDD and I’ve also worked with redux but I’ve never combined the two before. This post will be a little bit more stream of consciousness than normal and is basically me documenting an approach I’ve tried out. Redux is not an area I’m an expert in so I’d welcome any feedback from people reading this post with better ideas.
I won’t discuss driving slice development with TDD
Most of the code around slices is purely functional, taking an immutable starting state and returning the next sate. This is fairly easy to test with standard techniques.
Onto components
Where I ran into bigger problems was around testing the components. I had these partially covered with end to end
tests running in cypress. I also won’t cover that here but learntdd.in
has a good tutorial
on outside in testing with cypress for react.
My problems started when I wanted to test a component in isolation. One of the main responsibilities of my components is to dispatch the correct events to redux. So this was absolutely something I wanted/needed to test.
Following the tutorial
I was using the redux essentials tutorial as a place to experiment. The tutorial covers building a system for writing and displaying posts stored in memory. As part of following this tutorial you want to build a component that’s responsible for creating new posts.
A new post would be represented by a react action called postAdded
. The form component we end up creating would then
probably end up looking something like this:
export const AddPostForm = () => {
const [title, setTitle] = useState('')
const [content, setContent] = useState('')
const dispatch = useAppDispatch();
//...
const onSavePostClicked = () => {
// ... some logic to check if save can be clicked
dispatch(postAdded(title, content));
//...some logic to clean up after the save
}
//...
};
The first test touching redux
But how do we drive this with tests. The very first thing I wanted to test was that no event was dispatched if the user clicked save without filling in any post content.
So I wanted a test that looks like this:
import React from 'react'
import {store} from "../../../app/store";
import {AddPostForm} from "./AddPostForm";
import userEvent from '@testing-library/user-event';
import {screen} from "@testing-library/react";
test('the posts must have some content to be saved', async () => {
const testStore = makeTestStore(store);
testRender(<AddPostForm />, testStore);
await userEvent.click(screen.getByTestId('savePost'));
expect(testStore.dispatch).not.toHaveBeenCalled();
});
I didn’t know exactly what makeTestStore
or testRender
would look like but I knew I wanted to be able to make
assertions about dispatch so I ended up creating something like this:
import { render } from "@testing-library/react";
import { Provider } from "react-redux";
import {ReactElement} from "react";
import {store as rootStore} from "../app/store";
import {BrowserRouter} from "react-router-dom";
type Store = typeof rootStore;
export function testRender(tsx: ReactElement, store : Store) {
return render(<Provider store={store}><BrowserRouter>{tsx}</BrowserRouter></Provider>, {});
}
export function makeTestStore(store: Store): Store {
const originalDispatch = store.dispatch;
const testStore = Object.assign({}, store);
// Swap out the dispatch for a jest function so we can spy and assert on it
// @ts-ignore
testStore.dispatch = jest.fn(originalDispatch);
return testStore;
}
I can now write a very minimal component and make the test pass.
The next test touching redux
Next I wanted to test that it can actually dispatch a populated action with the data from my form.
The interface for the action is something like this:
interface PostAdded {
id: PostId,
title: string,
content: string,
dateAdded: string,
}
For my first test I wanted to make sure that title
and content
came from my form correctly. The other fields
are generated by the action creator and would be covered by some other tests.
For this I wanted a test case to look something like this:
test('It can dispatch postAdded', async () => {
const testStore = makeTestStore(store);
testRender(<AddPostForm />, testStore);
await userEvent.type(
screen.getByTestId('postTitle'),
"New Post",
);
await userEvent.type(
screen.getByTestId('postContent'),
"Some Content",
);
await userEvent.click(screen.getByTestId('savePost'));
expect(testStore.dispatch).toHaveBeenCalledWith(actionContaining("posts/postAdded",{
title: "New Post",
content: "Some Content",
}));
});
I created a helper function for testing these actions called actionContaining
to save repeating myself. It’s fairly
small and built on top of jest’s expect
:
export function actionContaining(actionType: string, expectedContent: any) {
return expect.objectContaining({
"payload": expect.objectContaining(expectedContent),
"type": actionType
});
}
Did this all work?
After adding the helper functions above (testRender
, makeTestStore
, and actionContaining
) I’ve been quite
comfortably driving my component development with tests. I’m still not sure if there’s a better or more idiomatic way
of doing this kind of testing so I would love to hear alternative suggestions or feedback on this approach. Send me
a message.