React component contract driven testing
Finding the correct way to test a React component is no simple task. In the course of my career I have tried various techniques, some of which were useful and others that I now believe were a big mistake. In this post I would like to share my approach when testing a React component.
A simple component
Let’s begin with a small component. In the next gif you can see a modest element, used for example in an e-commerce site. The component shows the number of the items present in a bucket, a label, and a button that can take me to the checkout page. The component also presents a visual effect that resembles a heart beat for the items number. I can call it item counter (ItemCounter in code examples).
The main characteristics of this component are:
- an items number
- a label in the center
- an action to perform when the button on the right is pressed
- a visual heart beat effect
- the color of the heart beat effect
What else? are there side effects? Does it need to load some data on component mount / unmount? We can manage these after in the post.
Ok, I’ve now got all the necessary knowledge to implement it.
Initial step: writing tests
In order to write the code for this component I start with a TDD approach, and the first thing to do is to write tests. But what tests do I have to write to ensure that my component works as expected?
There are a lot of things I can test such as checking that output html is as I expect. I experimented this in the early approach with React. The first time it seemed to be a good solution:
- write a component html output in test file
- write all the possible output cases, for example changing a prop
- write a component that solves all tests
What is wrong in this approach? Every single change on a css brake my tests. And it was frustrating!
But this experience teach me something:
Tests a single feature in a most isolate way as it possible
This means that if my component receives 4 props I try to test each prop regardless of the others. For example, in the item counter component I write a test for the items number, one for the label and so on. This also means that I don’t test the side effects that a prop can generate, for instance I don’t check that if the label prop is provided at the item counter component the button is correctly rendered.
Testing single features independently does not provide a global view on the component. But on the other hand this practice stressed me out enough to build decoupled prop handlers, and this is a good idea.
What to test?
Before doing anything I have to think about what the features of the component are and what it exposes to the rest of the world, for example starting from public APIs. This exposure could be interpreted like a contract between the component’s creator and the component’s user.
Synthetically a contract is a list of agreements between two or more parts. In the same way, the features of React components are a series of common intentions that the component implements.
Thinking about what to test means thinking about the contract that the component grants to his users.
Let’s start with the list at the top of this post. To test React components I use enzyme. And I found it very useful.
This is quite simple, provide a number and check that the number is correctly rendered.
Off course the test could be more sophisticated, testing that the number 2 appears in the correct position and not in the label for instance.
Other things? Yes, the itemsCount prop should have a default, zero for example, so test this situation too.
And i think that is all for itemsCount prop.
For the label text the workflow is the same, so go on to the next feature.
When a user clicks on the button a provided callback should be called. To test this case I provide a spy created with sinon to the ItemCounter component. And after simulating a click on Button I expect the spy has been called.
And what about a default for onGoToCart prop? onGoToCart is a function so providing a default is not possible, but React with isRequired contract grants that a warning appears when nothing is passed. The component is sort of a statically typed function, so I don’t need to test the validity of the input. I think this is an important lesson that I have learnt using React: the pattern that React introduces is highly testable in a unit way, for instance most of the user interaction is testable in a simple way by simulating events on components.
Is it a good thing to test the style of a component? The first answer could be yes, because the rendering is certainly fundamental: it’s what users see that we have to care about. But testing style is not simple because styles are processed from browsers and in a unit test I can’t ensure that the result is exactly what I want to see. Adding tests that confirm that a style is as expected means duplicate style implementation in a component and in its test. This will make it brittle. If I add another style, I would have to put the exact same code in my test. And this is quite boring and useless because it brings me to a permanent test refactor that reduces the developer’s confidence about a test’s real utility.
The ItemCounter component has a prop to provide a color for the heart beat effect. Is it important in this case to check that the color is correctly applied? Of course it is. It’s a feature of my component.
A side effect can be something related to the lifecycle of a component, a function called on componentDidMount or componentWillUnmount. It’s possible to look at this element as a part of the component’s public APIs. So it becomes meaningful to test it. Enzyme enables me to force React component’s lifecycle methods using instance API. In this way, using a sinon spy, I can ensure that a certain function is called during component lifecycle.
In conclusion I believe that many items in React component can be tested so a useful first step could be to find the contract that your component exposes. Finding it means describing in a complete manner how the component works. In this way only a little part of what you are testing will be styles and layouts that you have to check manually.