Building a React/Redux app with Test Driven Development

Gilly Ames
Jul 27, 2017 · 8 min read

TL;DR: How do I test my React/Redux App? Here’s an example repo with tests.

Motivation: You want to write a production app using React/Redux. You want to test this at each layer and be able to extend the app easily. You like test driven development. You like the look of these async actions, with mocks for the services.

Prerequisites: You’ll need a basic knowledge of React and Redux. We use redux-thunk in this example too, and knowledge of Jest or a similar testing framework will be useful.

Deliberate omissions: I’m not going to go into detail about enzyme testing — it’s a large topic with many fine tutorials available. We’ll focus on services, actions and reducer testing here.

Introduction

In this tutorial we’ll build a very simple, but easily extendable React/Redux App. We use the React Slingshot project for a quick start.

The app we will build displays repository information using the Github API. It has a service layer for API requests, and uses Jest to test services, actions and the reducer. The commits in the repository are organised so that they are a step by step guide to build this app using test driven development.

Cloning the repo.

git clone https://github.com/sandwichsudo/tdd-react-redux.git

This gives you the finished project.

Install yarn if you need to. Get the dependencies using

yarn install

Start the project with

yarn start

This should start the app at

localhost:3000

Have a look at the app structure in the directory. It’s a bit different to the slingshot app. Slingshot collects components, actions and containers at the top level. There is nothing inherently wrong with this, and you can keep to that structure if you’re happy with it. We’ll extend it slightly for this example. We will put any components, actions and reducers used by a single page inside the container directory for that page. You can see this in the finished example:

RepoSearchPage Structure

When we make components, actions or reducers that are reused, we will use top level component or reducer directories.

To explain the advantages of this, you should understand our motivations:

  • We want to know where to put new things. Components only used on this page are put in this folder. Constants only used for this page are put in this folder. Simples.
  • We want to keep it easy to find things. As you develop your app, you’ll create dozens of components. Alphabetical order under /components will only take you so far. You could group similar components together under subcategories. You might find that helpful for actions too. Also reducers. Instead of trying to keep the varying directory structure in line across components, actions, reducers etc, we use the action, reducer, component directory structure for each page.
  • We want to avoid dead code. If we remove this page, we want to remove any code only used by this page. This structure makes that very easy.
  • We want to avoid over-engineering components. If we need a component that prints out a list of repos, we can make a RepoResultsList component. However, where we spot common patterns, we can make a reusable component. This means we won’t shoe-horn ‘similar’ components together unless it’s actually useful to do so.

Ok that’s probably enough motivation. Let’s write some code.

Commit 1Cloned react-slingshot repo. Removed demo app.

To follow this tutorial step by step, start with a branch at the first commit

git checkout step-1

That will give you the project immediately after cloning React Slingshot and removing the example app.

For each commit there is a branch available for this, so if you want the finished code for Commit 5, do

git checkout step-5

Commit 2 Single page with static content.

To get started, we first set up our reducer and put some static data in the initial state. This allows us to see a static version of our app and get some idea of the data we will need to feed into the state to display it. For now we are just going to display a list of repos. We’ll look later at showing loaders and error handling. We won’t spend a lot of time on this as there is a fair amount of boiler plate involved, but the key bits of code from this commit are:

You can follow the changes on the commit if you want to practice doing this yourself, but as this is all basic React and Redux, we’ll do

git checkout step-2

and go on to setting up our ajax utility to make our api requests.

Commit 3Added XHR and test for repo search.

Next we add axios to allow us to make api requests. We set up the Github API as the base url, and we also add an interceptor to throw an error when the request status has an error code. The usefulness of this will become clear when we add our error handling later in the tutorial. You can skip over the specifics of this code if you are familiar with xhr, or use another library for making ajax requests.

The key change here is we have our ajax request utility and a mock we can use for tests which involve ajax requests. Note the path to the mock — Jest will find the mock so long as it is named the same as the module you wish to mock, and sits next to that module in a directory called __mocks__.

Now we can start to design our service layer. To start off with, we will add a test for the search service and assert that a request is made. We don’t actually want to send api requests as part of this test because tests which are reliant on live services can be flakey. We can use Jest mocks to request the mocked version of the xhr module we have made, and set up the mock to return a fake response. We can define that response for each test we wish to run. This will come in handy when we want to mock error responses.

We next write a failing test which checks that the xhr method which performs a get request is called with the url we expect. This may or may not be your idea of TDD, as it tests implementation rather than simply checking output. You can omit the assertions which test implementation if you prefer, but so that it is clear how to check implementation, we’ll do that here.

To run the tests at this point, you can checkout a branch at this commit using

git checkout step-3

and then run

yarn test

You should see

Test run at step 3.

This is good — we have a failing test. Now, when we write our implementation, we will have a check that our service invokes the appropriate methods and returns the desired content.

Commit 4Added repoSearch method to search service.

We make the test written in the previous step pass by calling the xhr get method with the appropriate github endpoint.

Our test now passes:

searchService test now passes

Commit 5RepoSearchAction test using RepoSearch mock.

Great, now we have a service which can make requests to Github and return us a list of repos. Let’s pass the returned list to the reducer to update the state.

Using a similar approach as in Commit 3, we will first create a test for an action that calls the service to get the search results. Create a mock for the search service and use in a test for an action which asserts the search service is called.

Test for action calling repo search

Commit 6Action which calls the service. Passing test using mocks.

Make the previous test pass by writing a function which calls the search service.

Commit 7Add action dispatch expect to test.

We’ll add an assertion for dispatch being called so that we know the repo list is being passed to the reducer.

Commit 8 Dispatching action when fetching items.

Dispatch the action to make the test pass.

Commit 9Test for reducer to update search results.

Let’s define how the action we just wrote will update our state. Define a test which asserts when that action is called, the state will be updated with the repos list passed to it by the action.

Commit 10Reducer case to update search results.

Make the previous test pass by implementing the reducer case.

Commit 11Adding test for fetching repos when search page component mounts.

Finally, we write a test that asserts that our action is called when the component is mounted. Notice how we’ve added an export to the class itself. This allows us to test the page in isolation, without mocking out its connection to the state.

Commit 12Fetching repos when search component mounts.

Use the React lifecycle method to call the api method from the search page. We now have a functioning app.

Handle offline/api unavailable error

We’ll use the same process to deal with the case where, instead of the api request returning data, it returns a request object with an error code like 401, or is simply offline. We’ve set up our axios instance to throw an exception in this case, so we can provide our mock with a Promise that rejects to write tests for this behaviour. But first, let’s add this error to the state.

Commit 13Add static error message to state.

Add a new property to the initial state and plug it into your component.

Commit 14Added test for dispatching request complete action.

Update the tests for for the request action to expect another action once the request completes. We will use this action in the error and success cases — this will allow us to hang a loader off of this action in future.

Commit 15Updating request action to dispatch action once request complete.

Make the previous test pass.

Commit 16Test for updating error when request completes.

Commit 17Reducer case to update the error when request completes.

Conclusion

We’ve now built a working application with React/Redux using TDD. Hopefully this will have given you some ideas about how to structure your applications for easy test driven development.

Appendix: Adding a loader.

That’s our errors taken care of. We’ll finish this off with a loader.

Commit 18Add loader to the page. Loads forever as state does not change.

We could extract this to a component if we reuse it. Or do it now if you’re keen.

Commit 19Test for request starting.

Commit 20Dispatch action to indicate request started.

Commit 21Add test to check loading property during and after requests.

Commit 22Update state loading property when requests start and complete.

That’s all folks! Thanks for reading through to the end

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade