My experience testing React applications - Phelipe Teles

My experience testing React applications

3 min.
View source code

Testing is not easy, it’s a trade-off: either you slow down new feature development by writing test to avoid regressions later, or you develop untested features and bugs will inevitably be introduced later.

But I can understand how that ends up happening: we have to develop new features fast and we don’t have time for tests. It’s not easy writing tests, but it’s a skill we have to develop. I try to do this with my side projects.

I started to write tests for my Python projects with the unittest and pytest libraries. I wasn’t very good at it as everything was new to me but I was obsessed with getting a ~100% test coverage badge in the README so I persisted. The benefits were obvious: I could refactor all I wanted later.

Sometimes tests got ugly because of all the mock/monkeypatch I needed to do to make them isolated, which was annoying but it was worth it. It was usually a seamless and rewarding experience.

But writing tests for React applications is a different beast — we’re testing user interfaces:

  • Things change, often asynchronously, which means we need to wait for them to appear or disappear.
  • We need to simulate user interactions.
  • There are a lot more network requests to mock, so it’s harder to isolate.
  • Integration test are more useful than unit tests, so tests are more complex.
  • It’s harder to debug (I didn’t find an equivalent of pytest --pdb).
  • It’s much slower.

In this blog post I’m going to talk about my experience with JavaScript libraries for testing front-end web apps.

react-testing-library

This library seems to be what everyone is using now, and I do see how that happened. It has a very consistent API and good documentation.

I liked that:

I disliked that:

But it got the job done most of the time.

I usually combined it with nock and msw for mocking network requests.

nock

The nock library works in node-only environment and has a well-thought, easy to use API.

In my experience, it has just a few unexpected issues like not working with axios out of the box.

msw

I later also tried msw after reading Stop mocking fetch by Kent C. Odds, and I liked it at lot. It can work in a browser (as a service worker) or node.js (by intercepting requests made by native modules).

The problems I had with it is that it does not recognize URL search/query params in the URL, you have to parse it yourself in the response handler. This is by design though, it’s just something I disliked.

Also, all mocks are usually in a separate file, like in ./mocks/handlers.js, which I thought made tests harder to read. It doesn’t have to be like that of course, but it’s response handlers can get pretty long so it’s a sane option.

cypress

All of these led me to try Cypress, and I’m inclined to say it is my favorite solution so far.

You don’t have to explicitly wait for anything to appear or disappear because it does so automatically by retrying it multiple times until a timeout is reached and it has a built-in API for mocking network requests with the intercept function.

But it’s not perfect either:

The benefits far outweigh these problems, mainly around debuggability:

  • Each step (assertion, interceptions) is traced and shown by the test runner.
  • You have access to browser debugging capabilities, so you have access to the browser DevTools.
  • There are snapshots of the UI state for each step.
  • Screenshots and videos of the test runs are recorded by default.

Conclusion

I think everyone would agree that Cypress is much well-suited for integration tests. I thought react-testing-library to be complicated for complex functional tests, so I’d prefer to use it for unit testing small component logic.