The React community is full of developers who are passionate about maintaining good development practices through testing. Many great tools and techniques are available to help you write tests for your React application, but to do it properly; you have to follow best practices.
This guide discusses the basics of testing in React, including why it is crucial, which tools are available, and the best practices for writing tests.
What is testing?
Testing is the process of verifying that something meets the provided requirements and produces the desired results. In programming, this means ensuring that your code works as expected.
There are two ways to test your application:
This type of testing is when a human performs tests step-by-step on an application without the aid of testing tools and scripts. QA analysts often use this approach when handling complex test cases that are not feasible to automate or involve scenarios that can only be validated once in a while.
Involves writing scripts that automatically check your code to ensure that the predefined conditions are correctly met. It handles multiple test cases that are easy to predict or tend to be validated repeatedly.
Why is testing so important?
As humans, we are prone to mistakes and occasional errors. When building software, some of these mistakes go unnoticed and make it to production, where they could result in minor glitches or massive bugs with severe consequences. It is essential to test your application before it goes live to avoid such situations.
Some other reasons for testing include the following:
- It helps you evaluate your application’s ease of use (how easy it is for a user to make use of your application).
- It verifies that all aspects of your application are working as expected. It can also check and fix unexpected conditions, such as piracy attacks or incorrect data types.
- It is an excellent way to provide up-to-date documentation of your code.
- It is cost-effective because detecting and fixing a bug during development is significantly cheaper than fixing it in production after it has affected many users.
- Testing makes adding new features to your application easier because if anything breaks in the old code, you can quickly detect and fix it.
Modern testing in React
Testing has come a long way; over the years, different principles have emerged on how developers should write tests. A modern, excellent guide to follow while testing your React application is using the “Testing Trophy” proposed by Kent C. Dodds. It broadly outlines four types of tests: static, unit, integration, and end-to-end (e2e) tests.
These tests help to keep your codebase clean by scanning for minor errors like typos or missing semi-colons. They can also provide hotfixes to improve your productivity. Popular tools for static testing include Prettier, ES Lint, and Mocha.
Write unit tests when you want to independently test each small, isolated piece of your code. These tests give you a granular view of your code’s performance and can run very fast since they are so small. They are frequently used in Test Driven Development (TDD), where you first write the test, then write your code to pass it.
Integration tests ensure that different components in your React application work together and interact properly. This type of testing is advantageous when collaborating with other developers on a project, as you will have different people working on various components. With integration tests, you can verify that these components work well together and even work with third parties such as APIs and databases.
These tests are usually performed after building your application. They test your application’s workflow from beginning to end, replicating user scenarios and checking for things like hardware, external dependencies, databases, and network connectivity.
Common React Testing Tools
There are several testing tools available for React. Here are a few popular ones:
Its features include:
- APIs such as expect(), and matcher functions like toBe(), toEqual(), toMatch() and toThrow(). These work together to evaluate test conditions.
- Rich exceptions (error messages) that give context to why your test failed.
- Mock functions to mock objects outside your test’s scope.
- Running tests in parallel and prioritizing tests to improve speed and efficiency.
React Testing Library
This testing utility tool adds APIs for working with React components to the DOM Testing Library. All React projects created with create-react-app have React Testing Library by default.
Its features include:
- Utility functions such as render(), renderHook() and cleanup() to work with react-dom and react-dom/test-utils in a way that encourages better testing practices.
- It supports querying the DOM the way a typical user would do it.
- You can use it together with Jest to write unit and integration tests.
This open-source test runner is written in Node.js. Every release of Mocha contains a mocha.js and mocha.css file and allows you to test asynchronous JavaSript in your browser.
Some of its other features include:
- Simple async support enabled by adding a done argument to your it() callback or returning a promise instead of a callback.
- Support for ES6 modules (new in v7.1.0).
- Hooks such as before(), after(), beforeEach(), and afterEach enable you to set up preconditions and clean up after tests.
- Ability to add any assertion library of your choice. The popular options are Expect.js and Chai.
Its features include:
- Ability to debug your code in your browser.
- Snapshots of your test as it runs, which enables you to see what happened at each step of the process.
- Screenshots and videos of failed tests to document bugs.
Automatically waiting for commands and assertions. This feature helps to prevent async issues.
Best Practices for Testing React Apps
Here are some best practices for testing React apps:
Test more functionalities, fewer implementation details
When building your React application, your codebase will be classified into two major parts:
- The parts that are visible to the end-user and contain functionalities that the user can interact with, such as the UI, state data, etc. For example, what will display on the screen when a user clicks a button.
- The parts that are not visible to your end-user, typically implementation details such as variable names, classes, props, etc. They can be anything, as long as they deliver the same functionalities the user expects.
If you write tests for a variable name (an implementation detail) and later update the variable name in your code without updating the tests, your code will still work as expected, but the tests will fail. This scenario is known as a false-negative.
Assume you did not change the variable name, but you have a function to display some text on the screen when the user clicks a button, and you did not call that function correctly (bad functionality). If you run tests on your implementation detail, it will pass, but your application will fail in production because its functionality is faulty. This scenario is known as a false-positive.
To avoid scenarios like these, it is better to test for functionalities that reflect how your end-user will use the app since these expectations rarely change no matter what updates you make to your codebase.
Multiple integration tests
In your React application, you will build many components that interact differently. For example, a button component can work with a contact form to get and upload user data, and the same button will work as a call-to-action on the homepage.
To ensure that these integrations work as expected, you should write integration tests to check how your components interact with each other in different contexts and scenarios and even with external APIs.
Test one thing at a time
When writing tests for your React application, to avoid conflicts and ensure that all the essential parts of your app are thoroughly tested, it is best to have a list of things to test for and go through the list one item at a time.
This practice will help to keep your test suites smaller and more manageable so that changes are easier to spot during code review.
Avoid unnecessary tests
Consider this example below:
In the code above, you test to see if a modal displays a message. For the modal to display that message, it has to be visible, so the first test is unnecessary and can be safely removed.
Developers are often encouraged to aim for 100% code coverage, but this could lead you down a rabbit hole and make you spend more time writing unnecessary tests. It could also result in missing out on important scenarios likely to have defects because you aim to ensure that each line of code is tested.
It is best to focus on writing tests that truly matter to ensure your application works as intended, then continuously improve on that code and its tests.
Testing is an essential part of the development process. However, if you test for the wrong things - or even worse, don’t test at all - you will spend too much time trying to figure out why something is not working correctly.