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.

Discuss this post over on reddit