What is unique about this approach or solution?
- It gives you confidence when refactoring code as you avoid regression bugs when adding new features.
- It adds documentation to your code automatically. Tests cases are a good way to understand what the code does.
- It helps other team members to understand what you did.
- It helps to find bugs that maybe you cannot see when running the app.
What is the business impact?
By writing tests, at the beginning you may feel that development is slow, but after two weeks / one month, the code will be highly maintainable, as we can add / remove features with the confidence that new features won’t break the application. This is a big impact to the business, because of the time savings by avoiding regression bugs. Another big efficiency is the on-boarding of new team members to the project is easier, as they can read the tests and easily understand what the code does.
But what should we use to test our React applications? A few months ago we tried using Jest with Enzyme to test our apps and we found it a bit tedious. That is because Enzyme gives us the tools to test the implementation of your component, which is not what we want. From my point of view, I understand that implementations should be checked in pull requests and code reviews, but not using unit tests. Of course, you can test it if you want.
That means for me it doesn’t matter how it was done, what matters is how it works and how it is shown. When you buy a car you don’t check how the brakes work , you just press the brake and you expect the car to stop. We take the same approach to test our components with react-testing-library.
What react-testing-library is?
React-testing-library is a library developed by Kent C. Dodds, which uses the DOM Testing Library as its core, enabling us to query DOM nodes and check what it contains as well as interact with them (firing events for example).It’s part of the testing-library family, which means that there is also a testing library version for Vue, Angular, etc.Writing tests using it is simple, but before write some cases, let’s talk about TDD.
TDD: Test Driven Development
Often when we start to create a component, we just read our customer requirements, check the design, create a MyComponent.tsx (or jsx) file and start to write the implementation. Once we have finished, we start to create tests to check our functionality.
That could be fine, but in most of the cases doesn’t give us a global vision of what we need to do and which cases we need to check in order to avoid bugs later.
Using TDD we will write the tests first, and then we will write the code to pass the tests. So a good way to go is check the customer requirements first, and write a test case for each requirement.You will notice that while you are writing test cases, you will discover other cases. This approach also forces you to consider boundary cases more carefully which will avoid issues later.
For example,let’s suppose that our customer wants the following simple feature:
- An element with two inputs, a button and some text
- It should sum the two values when they press the button and show the result
Starting from that point and using TDD we start creating our tests file, our component file (empty) and write some tests cases that will check our customer requirements are accomplished:
But … this forces you to think about a few things before you write a line of code:
- What if the values are not numeric?
- What if there are not values in the one or the other inputs when you click the button?
- Should there should be any initial text?
- Should there should be any initial values in the input?
- Is there is a specific text to show in the button?
So at this point, you can talk with your customer and ask those questions. And in doing that process you can clean the task you have to accomplish and properly bound scope of your task, and at the same time maybe your customer will more thoroughly consider behavior he doesn’t want or some cases to add.
So let's write the tests for our component using react-testing-library. If you create a project using create-react-app, just add @testing-library/react as dependency and import it in your tests.
The main utility we have is render, which enables us to render our component. The second utility we need to use is cleanup, which unmounts React trees that were mounted with render before each test is run. The last utility we will use is fireEvent, which enable us to fire events.
After run npm run test:
Our test should be in red since we haven’t written the code yet. Let’s make our test pass:
Now we need to test that our component has two inputs. We can do it using different approaches, since react-testing-library gives us many ways to query the DOM. My favorite way is to query using test-id, because that way doesn’t depend on the style or the content of the element.
So let's write our test first:
Using getByTestId, we can look for any element with data-testid attribute that matches with the value. So in our component, we will do the following:
Now we can do the same with all the other components. Notice that at this point, the style and behavior do not matter. We are just testing that the component does what our client wants.We simply create tests that check the behavior, to do that we will use fireEvent and extend our expects to check the text content by importing “jest-dom/extend-expect”:
Then we update our component:
But hey, unexpected value!
As you may have noticed, what we are doing is concatenating strings and not summing numeric values. Let’s quick fix that by adding parseInt when call the setValue
And now our tests pass!
So we haven’t checked the UI, but we can be sure that 1 + 1 will be two.
But what if we try to sum decimals? 1.5 + 1.5 should be equal to 3 but …
And that is because we used parseInt. Let’s change it by parseFloat and now our tests will pass:
This is a very basic example, but it shows how the automated testing approach helped us get the component to behave as we need it to by testing for edge cases and appropriately handling data types. what I want to put in value is that without the test, if we never have checked a decimal value in the UI, we will never noticed that our component was broken.
One of the intentions of writing tests is to add strange cases to check how our component behaves.
Here is the Codepen that shows our simple example.
Tips when testing your components
- Write pure functions and move them outside the component. That will make them easy to test.
- Try to split your code into smaller functions and components. It’s easy to test small pieces and then create complex structures using them because that gives you the confidence that it will work as all the small pieces have been tested. Divide and win!
- Try to reach 100% coverage. That will help you to find all the edge cases.
- Add limit values to your test, try to break your component
Debugging your tests
Tests are code too! Sometimes they will need to be debugged as well. When you create tests, in some cases you need to debug them to check why it gives you a red. Some IDEs have it integrated but in our case we use chrome.
- Add this script to debug tests in your package.json:
"test:debug": "node --inspect node_modules/.bin/jest --runInBand"
- Add a debugger statement wherever you want to stop the test execution and inspect values
- Open chrome and go to chrome://inspect and open the dedicated Dev tools for Node:
- Run the script. You will see that the execution will stop in your debugger sentence and you will be able to inspect the values.
That’s all! happy testing!