Step by Step TDD with React, Jest and Enzyme

Andrew Berry
8 min readMar 18, 2021

Note: Enzyme is not compatible with React 18. Please see my updated article:

When I ask most front-end engineers about Test Driven Development (TDD) the overwhelming response is that it feels daunting or impractical. In my experience, many engineers don’t know where to begin, which is totally understandable. How do you write tests before you have written the implementation? Where do you start?

This article hopes to clear up some common misconceptions and show you that TDD is an approachable and straightforward process. Even better, with a little bit of practice, it has the potential to make you a better developer.

Why is TDD helpful? At a high level, it provides a shorter feedback loop that speeds up development and produces better code. It also helps you break complex problems into smaller testable pieces, and refactor with confidence.

At a high level TDD follows the sequence:

  1. Add a test that asserts a small piece of known functionality
  2. Watch the test fail
  3. Write code to make the test pass
  4. Continue refactoring, or repeat the sequence for the next piece of functionality

Before getting started it’s worth noting that:

  • In TDD you don’t need to write all of your tests up front — you write them as you go.
  • You don’t have to write a perfect test before writing any code. It’s a constant feedback loop and you will often iterate on both as you work.
  • It might feel like more work the first few times, but the faster feedback loop will ultimately speed up your development and catch more bugs as you write code.

I also think TDD will improve the quality and effectiveness of your test suites. Writing tests after the fact is usually a chore, and a lot of nuance will be missed. By treating unit tests as a development tool and form of documentation you can unlock their full potential.

I hope the following walkthrough gives you the confidence to try TDD.

Note, although this example uses Jest, Enzyme and React, the general steps and concepts are applicable in other environments. I’ve kept the problem and code simple since the focus of this article is on the TDD process.

The Problem

Imagine the following task:

Build a component that allows a user to view and update their account preferences. On save we should persist their selection to the backend.

To keep things simple let’s assume we already have code in place that interacts with a backend API. The developer tackling this problem is only concerned with building the UI for the user interaction.

UI wireframe

The Solution

To begin let’s stub out the UserSettings component and spec with some boilerplate.

components/UserSettings/index.js
components/UserSettings/index.spec.js

Now we’re ready to run the spec. As you can see it fails immediately because we haven’t written any tests.

A good place to begin is by testing that the UserSettings component renders. It’s an easy way to make sure we’re exporting the component and have stubbed the files correctly.

Before going further we should stop and think about the feature and how to split up the requirements.

Build a component that allows a user to view and update their account preferences. On save we should persist their selection to the backend.

There are a few key things our code must do, but at this stage we don’t need to worry about most of them. In fact, there isn’t much we can do until we render a basic list of settings.

It renders a list of settings

Before we write any real code let’s add a test for the first piece of functionality we’ve defined. Think of this as a building block. It’s ok to make assumptions and start simple. This test will also evolve as we code!

Developer intuition tells us that we’ll probably end up with a <form>, which means our options will likely be wrapped in <label> elements. We can also start designing our component's API by planning for an options prop.

As you can see this first test is very simple and is somewhat lacking. But that’s ok. It gives us the starting point we need. The test will fail immediately as expected — this is the start of our feedback loop.

Let’s update our component and make the test pass by rendering a label for each item in the options prop. This should do it…

See L3–L9

Not so fast, now our first test fails.

This is actually great information. We immediately see that we should set a default value for options to prevent this error. If we only tested the happy path, or had no coverage at all, we would have missed this detail.

See L14

By adding options to defaultProps we’re able to fix the test and continue.

Obviously just rendering a label isn't going to be enough for the functionality we need. Rendering a checkbox so the user can change their selection would be much better. Before we update the code, let’s update the test with our implementation in mind.

And now we can add the code to render the checkbox.

See L6

With this change our test passes again, and we can move on and add the next piece of functionality. The key thing to emphasize is that we are working iteratively and breaking the problem into small testable pieces.

It shows the user their saved settings

A key requirement is that the list should show the user’s saved settings. It’s very easy to add this functionality on top of our foundation.

Let’s add a new test. Similar to the last test we need to update the component’s API. Let’s add a savedUserPrefs prop for now and make it an array of ids. It's possible this will change to something more robust later, and that's ok!

It’s worth pointing out that our tests form a suite. We don’t need to test the checkbox values again since the preceding test has it covered (pun intended).

We also see a pattern emerging. Most of our tests need a value for options so let's add a setup function to keep things DRY.

We still need to make our new test pass. We can do this by conditionally toggling the checked attribute of each input using state.

See L2 and L11

Hopefully you can see that working this way helps us think about the functionality, as well as the implementation.

Let’s finish up our component. We still have some core features to add. We’ll cover these quickly since the process is the same.

It updates the selection when a checkbox is clicked

We have a list that shows the user’s current selection, but the user isn’t able to make any changes. Let’s fix that.

The test itself is pretty straightforward. When a checkbox triggers a change event, e.g. when it is clicked, we expect the checked attribute to be toggled. You might be wondering why we need to test this behavior. Shouldn’t it just work? Since React is controlling the value via state, the input is now a controlled component, which means its value will only change if state is updated.

We can implement the required functionality by adding an onChange handler to the component. This handler updates the checked state based on the event data.

See L3–L11 and L21

It saves the user’s settings on submit

Things are coming along. The user can change their preferences but we don’t have a way to save these changes yet. Let’s keep this component “dumb” and pass down a function that will be called on save. Since it will be an HTTP request let’s assume it will return a promise.

The behavior can be tested using a mock function and simulated events, which is a great way to test this type of interaction.

A button and onSubmit handler are added to the form, and onSave is called with the correct arguments when the form is submitted.

See L15–L18 and L32

It shows a saving message

And finally let’s give the user some feedback by updating the save button to say “Saving…” while the request is in flight.

Note the use of async and await Promise.resolve() which is used to flush the promise queue before retesting the default state.

We can add the behavior by adding an isSaving state to toggle the button text, which we will update before onSave is called and after it completes.

See L3, L15, L17 and L35

And we’re done. Our component meets the business requirements, has good test coverage, and can be easily refactored or built upon in the future. TDD gave us a fast feedback loop and we were able to build and test without leaving our editor to do any manual testing. We definitely want to test in the browser as well, but TDD has already done a lot of the heavy lifting.

Here’s a summary of the tests we wrote while building the component:

  • It renders a list of settings
  • It shows the user their saved settings
  • It updates the selection when a checkbox is clicked
  • It saves the user’s settings on submit
  • It shows a saving message

Without trying we’ve effectively written acceptance criteria for our feature!

The full component and test source can be found here.

Conclusion

Hopefully this has given you some pointers on how to approach TDD and its benefits. Remember that code and tests evolve together— you don’t need to write the perfect test suite before you begin coding. I encourage you to see TDD as a tool that will make you a more efficient and effective developer. Don’t overthink it.

If this were a real component I’d go back and do some final tidy up. For example, some destructing in the map callback might make the code more readable, or I might rename some of the props. The great thing is that I can refactor with the security of working tests!

Lastly, for fun here is a GIF of how our code evolved as we went through the TDD process.

--

--