TDD your React components
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.
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.
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
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?):
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)
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.
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.