/react-testing-techniques

Testing effectively using a user-centered approach

Primary LanguageTypeScript

React Testing Techniques

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.

Tools of the trade

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.

Why do we write tests?

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!

Guiding principles when writing tests

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.

Don't test implementation details

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).

Test your components as a user would

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.

Don't be obsessed with code coverage

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!

Push business logic into pure functions rather than UI components

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.

Testing Techniques

Now that we understand why we test the way we do, let's go over 12 techniques you can apply now.

  1. Setting up React Testing Library
  2. Snapshot testing vs. traditional unit testing
  3. Difference between queryBy, getBy and findBy queries
  4. Checking for existence of an element
  5. Waiting for removal of an element
  6. Waiting for something to happen
  7. fireEvent() vs userEvent
  8. Mocking an event handler
  9. Avoid mocking by using Mock Service Worker
  10. Overriding MSW handlers
  11. Testing page navigation
  12. Suppressing console errors

Getting Started

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/.

Running Unit Tests

Execute one of the following command to run unit tests.

yarn test # interactive mode

# OR

yarn test:coverage # non-interactive mode with coverage information

Running End-to-End Tests

yarn start # starts a local server hosting your react app

# in a difference shell, run cypress
yarn cypress:open

Running Storybook

yarn storybook

Running In Production Mode

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

Screenshots

Home Page

Home Page

Checkout Page

Checkout Page

Orders Page

Orders Page

References

Testing Best Practices

Jest

React Testing Library

Storybook

Mock Service Worker

Cypress