TDD your React components

Volodymyr Stelmakh
6 min readDec 30, 2018

People don’t like writing tests. It feels like a waste of time to the most of us. Especially front-end developers. Why bother if I can just go and check it in a browser? And TDD is even worse, why do I write tests before code, intentionally limiting myself? I would have it done three times quicker you say.

Well, you have a point there. But do you feel confident about your code?

For me, tests are all about confidence. You can check it in a browser, but you barely check all the cases manually. And what happens when your code changes over time? Do you go and check it all over again? Yeah, sure.

You may feel uncomfortable now. That’s ok. “I don’t know what to start with”, you say. That’s fine too.

Start small

Let’s have a look at your React app. I bet you have a handful of shared components there. Inputs, buttons, you name it. They are relatively simple, you use them all across your application and sometimes (more often than you’d want too) you modify them to fit your current needs. You add one thing and it breaks something else. Not good.

Those components is a good place to start from.

We need a playground, so let’s set up a project. To make it easier, we’ll use Create React App.

If you never used it before, please refer to the link above for detailed instructions. I’ll keep it short here

$ cd %your_workspace_dir%
$ npx create-react-app test-components
$ cd test-components
$ yarn start

If you did everything right, it will take you to http://localhost:3000/ page and you’ll see this:

Nice, we have the project up and running. Now, what do we need to start writing tests?

Test runner, assertion library, mocking library. Those three we got covered by Jest. It ships with create-react-app, so no actions required. Next, we need to render react components in our tests and interact with them. Enzyme will handle it for us.

yarn add enzyme enzyme-adapter-react-16

We also need to setup it. Add a setupTests.js file to src/

import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });

Done!

Show me some code already

Ok, let’s implement a component. It will be a controlled input, so it should receive value and onChange handler. Plus a bunch of additional features, like error and disabled states would be nice to have. I know you’ve done it before, but this time we’ll go the TDD way.

First of all, create a components/Input folder inside src/ . This is where the component will live. Now, let’s add a file index.jsx with the following contents.

Our Input component renders nothing yet

It returns null because we’re not ready to implement it yet, we just need a component that we can import.

And let’s get rid of the default CRA stuff in App.js We can also use our newly created component there.

Red, Green, Refactor

We want our Input component to actually render an input element. That would be the first thing to check.

Red

Create an index.test.js file inside the component folder.

The simplest test case

Run yarn test to see how it fails

What did we do? We created an ‘Input component’ test suite with a ‘renders input’ test case that shallow renders our Input component and checks if it has an HTML input element.

Shallow rendering is useful to constrain yourself to testing a component as a unit, and to ensure that your tests aren’t indirectly asserting on behavior of child components.

This is a red step. We added a failing test that describes the functionality that we plan to implement.

Green

But don’t want it to fail, right? The Input needs to be fixed.

In the terminal, you should now see the test passing

The test suit now passes

This is a next, green step. We’ve made the smallest change required for a test to pass.

Refactor

Now we should think about how we’re going to improve the functionality we just implemented. This step is called refactor. There’s nothing we can do yet really, so we’ll skip it for now.

Repeat

Now to the next feature. For example, we could add a value prop and expect the Input to show that value. So the flow will look like this: first we create a test (red, remember?):

The test case for input value

Here we shallowly render a component and pass it the value. Then we check if the underlying input element value equals the one we passed.

It’s quite expected that the test case fails.

Now we will fix it. Applying the smallest change required. (Green)

The smallest change required

This is what makes TDD different. Without it you would probably consider storing the value in an internal state. Just in case, right? Even if you won’t, there’s still a chance you would over-engineer it. But with TDD, you make it as simple as possible.

There’s nothing to improve yet, so we think about the next feature we want to add. Easy, right?

Finishing the component

onChange prop is a must have for the controlled input. I like when onChange receives a string instead of an event. So let’s add another test

This one may require some clarification. jest.fn() is a simple function mock. It track calls so you can assert them later. We pass the mocked onChange handler to our Input component. Then, we simulate the input change event by passing { target: {value: newValue} } (mimicking a native event structure) to enzyme’s simulate function. It’s expected that onChange is called exactly once with the given value as the argument.

It’s red again! Let’s make it green.

The onChange expects to receive string , so we add a handler function where we take a value from an input change event.

Now the test should pass.

Two more features left: disabled and error states. When the component is in error state, it should have red borders and text. So it’s just a matter of adding a CSS class. Again, we start with a failing test:

And then we fix it:

Well, it’s just a CSS class name and we didn’t test the exact implementation, like border or text color. You definitely can do so, but in general case it’s better to test logic and styles separately. Styles require manual verification, you just need to go and check how the component looks like (This is one of the things we, humans, can do better than computers). There are tools that can help you to do that, e.g. storybook.

The last thing to add is a disabled state. When an input is disabled, it should have some opacity and it should ignore all interactions. So we’ll check that the class is added and onChange handler is not called.

The test:

And the updated component:

Notice that we did some refactoring here. We updated the way we set the class name and also updated the onChange handler. And we’re confident that we didn’t break the existing functionality, because tests are still green. This is the power of TDD.

Now the component behaves exactly as we want it to. All we need is to add some styling, and it’s ready to be used.

Simple input styling
Apply css to the component

A complete code example (along with some dummy functionality to use the component) can be found here.

That’s it

Red, green, refactor. Three simple steps, that will allow you to write laconic, predictable code that will do what you expect it to do.

Thanks for reading!

UPD: Updated the refactoring part, thanks Kjetil Klaussen for noticing my mistakes.

This story is published in Noteworthy, where 10,000+ readers come every day to learn about the people & ideas shaping the products we love.

Follow our publication to see more product & design stories featured by the Journal team.

--

--