This project demonstrates best practices in testing React applications by implementing a realistic online shopping application. It is also the repository for my Medium article titled React Testing Techniques.
P.S. If you find this project useful, please show your appreciation by starring this repository.
Unit & Integration Testing
-
Jest - a testing framework designed to ensure correctness of any JavaScript or TypeScript codebase
-
React Testing Library - a testing framework for React components that encourages better testing practices
-
Mock Service Worker - a framework to mock APIs by intercepting requests at the network level. It allows us to reuse the same mock definition for testing, development, and debugging.
End-to-End Testing
- Cypress - a testing framework for fully built Web applications running in a browser
Manual Testing
- Storybook - a tool that helps build components in isolation and record their states as stories. Stories make it easy to explore a component in all its permutations no matter how complex. They also serve as excellent visual test cases. Storybook testing can also be automated. For details, look at the Storybook documentation.
This project was bootstrapped with React Accelerate.
For me, writing tests is about building confidence in what I am delivering. Tests provide a mechanism to verify the intent of my code by exercising it in various ways. Moreover, they give me the confidence that I have not broken anything when I refactor or extend the code to meet new requirements. The last thing I want is to get a call at 3:00 AM to fix a bug that has crashed my app!
The principles listed in this section are based on an article by Kent C. Dodds titled Write tests. Not too many. Mostly integration. Kent is a testing guru with very good guidance on how to test effectively. I have listed several of his useful articles in the references below.
So without further ado, let's jump into the guiding principles.
If your test does something that your user doesn't, chances are that you are
testing implementation details. For example, you may be exposing a private
function just to test your component. This is a code smell – don't do it. A
refactor can easily break your test. Another example is using certain APIs of a
React testing tool called Enzyme, e.g. its
instance()
, state()
and setState()
APIs. Stay away such tools, instead use
tools that make it harder to test implementation details (e.g.
React Testing Library).
The classic testing wisdom was to write a lot of unit tests to test individual "units" of code. We used to isolate our components from their environment using mocks. It was like testing a fish's swimming abilities out of the water. This approach still makes sense for pure functions. But for UI components, which depend on communications with surrounding components, mocking reduces our confidence in their integrations.
For this reason, the latest thinking is to test several units together to recreate real interaction scenarios, hence the name "integration testing".
This brings us to the guiding principle which is the foundation of the React Testing Library:
The more your tests resemble the way your software is used, the more confidence they can give you.
For example, drop a couple of components under a <Context.Provider>
to test
real user interactions. You could also use
Mock Service Worker to mock APIs at the network level rather
than excessively mocking at the component or service layer. We will talk more
about this in the testing techniques section below.
There is a tradeoff between time spent writing tests and code coverage. Some organizations put undue focus on code coverage. Unfortunately this sets the wrong goal for developers - after a certain point, the returns are not worth the effort. You start seeing developers gaming the system by writing meaningless tests.
Instead, focus on use case coverage. Think of all the use cases (including corner cases) that you want to test to feel confident about your code. This approach will automatically yield high code coverage. The tests in this project were written with use case coverage in mind and yet as a byproduct we have upwards of 90% code coverage!
For example, a Shopping Cart UI component should not compute the cart total. This should be pushed to a pure function because it is easier to test. Even better, push it off to the back-end where more sophisticated calculations can be performed without complicating the UI. See here for examples for pure functions and the related tests.
Now that we understand why we test the way we do, let's go over 12 techniques you can apply now.
- Setting up React Testing Library
- Snapshot testing vs. traditional unit testing
- Difference between queryBy, getBy and findBy queries
- Checking for existence of an element
- Waiting for removal of an element
- Waiting for something to happen
- fireEvent() vs userEvent
- Mocking an event handler
- Avoid mocking by using Mock Service Worker
- Overriding MSW handlers
- Testing page navigation
- Suppressing console errors
Note: If you prefer to use npm, please feel free to replace the yarn commands in this section with equivalent npm commands.
Make sure your development machine is set up for building React apps. See the recommended setup procedure here.
Execute the following commands to install dependencies:
yarn install
Execute the following commands to run the app:
yarn start
Now point your browser to http://localhost:3000/.
Execute one of the following command to run unit tests.
yarn test # interactive mode
# OR
yarn test:coverage # non-interactive mode with coverage information
yarn start # starts a local server hosting your react app
# in a difference shell, run cypress
yarn cypress:open
yarn storybook
Because MSW is disabled in production mode, you must first run an external API server. To do this, clone the React Test Shop Server repository and follow the instructions there to start an API server on port 8080.
Now build this project in production mode and start it using a web server like
serve
:
yarn build
serve -s build
- How to know what to test by Kent C. Dodds
- Write tests. Not too many. Mostly integration. by Kent C. Dodds
- Write fewer, longer tests by Kent C. Dodds
- Making your UI tests resilient to change by Kent C. Dodds
- Testing Implementation Details by Kent C. Dodds