ReactSmallTips: unit-testing-components
This article is PART II of another article that I wrote, recently, about good practices with unit tests. If you don’t know what I am talking about, I highly recommend that you take a look at the PART I: JS SmallTips — unit tests: good practices. While in the first post I discussed about unit tests in general (regardless of whether it is tests on react-components or not), having a good understanding of these concepts is crucial to fully understanding what we will see in this one.
First things first
As we know, unit-tests are tests performed on units. If we think about a react-component, they are just functions (it can be classes too, but not recommended). So it is perfectly acceptable that we deal with unit tests in components folowing the same principles and concepts we use to test a normal function or class. Therefore, applying those concepts while testing a component, will give us the same benefits we know.
What to test
When dealing with unit tests on components, some developers can feel a little intimidated and most of the time they may wonder: “There is so much going on in my component, what should I test?”. And thats true, sometimes your component can have multiple props, local states, global states, using third-party-libs, fetching data from an API, hooks, nested components, conditional rendering, etc. If that’s the case, you should first consider breaking down your component into smaller and simpler components. This will make your life a lot easier! After that, we can answer that question for each component we created: It depends.
I know that answer isn’t helpful. So, I’ll give you my tips on this one. Whenever I am testing a component, I consider 3 things that tests must cover:
- conditional rendering
- user events
- (re-)rendering (based on state changes and/or props)
With these 3 pillars, we can implement really effective unit tests on any component. To test components, besides Jest, we need more tools. Here I’m going to use testing-library lib, which is just a utility lib that provide us the necessary tools to unit-testing components.
Consider this simple Counter component
It will increment and decrement the count value as the user click on the buttons
Testing first rendering
Testing events and re-rendering
Now, let’s test some user events (clicks) and re-rendering. For this, we will use userEvent and async methods provided by testing-library
Some notes here. Whenever you test something in a component that causes a re-rendering, we should assert using async/await since the async methods return a Promise. See the testing-library documentation on Appearance and Disappearance and userEvent.
Testing props
Let’s make the Counter component receive a initial value as props, and test whether the props is being rendered as expected or not.
props being rendered..
What if the Counter component receive a function as a prop? How do we test it? Well, the same way we have passed a value to initialValue prop, we can also pass a mocked function as a prop. During testing, at the component level, it doesn’t matter if the function is actually executing anything, we just need to make sure the function is being called as intended.
Let’s add another prop to the Counter component. This time a function prop that will be attached to a button
Adding a function as a prop
prop (function) being called..
Testing condtional rendering
To exemplify how to test a conditional rendering, let’s make the component tree a bit more interesting.
First, let’s create a Page component that will get an initial count value from an async method and pass it to the Counter component as props
Although this is still a simple component, it covers several real-world scenarios when you have components that use hooks, async methods, conditional rendering, dependencies, etc. By the way, when dealing with a complex component, the first thing we must identify are its dependencies.
Here we have 2 dependencies. The service we are using to get the initial data (myService.getInitialValue) and the Counter component.
Maybe you are asking yourself: “What? Why is the Counter component a dependency in the Page component?” Well, we saw that a component is nothing more than a function, and a function is an unit, therefore a component inside a component is just a dependency, just like the myService.getInitialValue function. When the Page renders the Counter component, it is just calling a function. We know that, while testing, is a good practice to mock dependencies instead of calling the actual implementation.
This makes the unit tests simpler, easier to maintain and faster to run. Furthermore, we have already tested the Counter component separately, so there is no need to render Counter’s actual implementation during Page’s tests. So let’s mock the Page’s depenencies and test it..
mocking the Counter component..
testing the Page component..
The main reason why the test was so easy to implement, is that we mocked its dependencies, including nested components (Counter). There is nothing wrong with that. In fact, this is desirable. If you don’t want to mock nested components, you’ll still be able to unit-test your application, but your tests will become more complex and slow to run.
That’s why I recommend: in cases where you have a component that renders other nested-components. Start testing the nested ones separately and then, test the outer component that is wrapping those.
So, that’s it. These were my final basic tips (I think) to assist you in writing better unit tests regarding testing components in React. I hope you found them helpful. Thank you for reading this far.
See you next time!