What I've learned about testing React applications
Learning how to test React applications in a way that works for you and your team is challenging but worthwhile work.
TL;DR Testing on the front end is one of the most personalized aspects of development, with a variety of shifting tools and opinions, particularly for the React ecosystem. Be patient with yourself when you’re learning how to test. In my experience, it’s one of the more challenging things to teach yourself.
I spent a lot of time recently digging into React testing and came away with more knowledge than when I started, though I still have some lingering confusion. I did, however, develop a system that feels workable to me and makes my tests make sense with how I work. Currently, it seems that’s the best we can ask for as React testing is evolving at a rapid pace.
While it can be frustrating that best practices for testing React are in such flux, I’m hoping this article will shed a little light on the various options and be a step toward some kind of concensus.
Before I dive into the React-specific information, I thought I would share a few general thoughts about testing. Words of wisdom, if you will, based on my experiences. They serve me as reminders of why testing is important and the reality of what writing tests is really like.
Yes, you’re basically writing a stripped down version of the logic of your app all over again in a different framework.
There are some loose best practices that have been inherited from the backend, but folks seem to be even more splintered on how testing should be handled than they are on which framework you should use. What works is something you’ll have to decide with your team.
Writing tests requires thinking very differently about your app than you usually do, and will have you rethinking how you’re handling data.
Coverage is an imperfect gauge for quality. If you have not written “unhappy path” code (else statements, catch blocks, etc.) your coverage will be inaccurate. You can’t test code you haven’t written.
Writing tests does indeed increase the amount of time, code, and effort it takes to write an app. That’s not an illusion and it’s something to be accepted as part of the process because…
It saves an unseen amount of time and effort you would have put into debugging, especially during refactors. You won’t know how much you’ve saved because you won’t be doing that work. This is easy to forget.
When it comes to testing React applications, there are choices depending on how you approach your testing. Do you want to focus on integration tests? Unit tests? Or do you prefer functional tests? And yes, those terms have different definitions depending on who you ask. Learning about these options was a good reminder that React is technically a library and not a framework. That means there is a lot of customization possible and that is abundantly clear with testing.
- ReactTestUtils: React’s own built-in testing utility, which is no longer recommended even by the React team.
- Jest: the defacto standard library for React testing, built by Facebook specifically to test React applications.
Built on Jest
- React Testing Library: the currently recommended component testing library, it tests components the same way your users use them.
- Enzyme: a widely used testing library and competitor to RTL. It allows you to traverse and manipulate your components’ output.
- Cypress: end-to-end testing with time travel, snapshots, and a browser-based interface.
- Puppeteer: end-to-end testing that runs headless Chrome and allows you to write tests that interact with the browser without running it.
A few other options can be found in the React docs’ Community section.
That covers most of the options for basic testing. There are also numerous supplemental tools that can be used, depending on your application. For example,
redux-mock-store is often used to mock the redux store and
moxios can be used to mock
axios for API testing, though there are other options for testing both of these tools.
Documentation & Notes
I found the documentation for most of these tools to be very dense, referring to concepts that are fairly complex without significant prior knowledge. When I tried applying the lessons from the basic tutorials I found, I was quickly frustrated that they did not align with the more complex code base I was trying to test. By “more complicated” I mean that I was using Redux and React Router on top of React, so in the real world, not actually all that complicated.
There were a few important concepts that were not clearly explained in the various documentations I read. Here’s what I’ve gleaned from the docs and learned from others who are also trying to figure this all out:
Individual files are run in parallel (unless you enable the runInBand option). So it is not safe for multiple test files to share a mutable data store. All describe and test blocks within a file always run in serial, in declaration order. This is important to note if you are mutating data between tests.
React Testing Library does not rerender the component when props change if you are working outside the redux store, even if you try to feed in new props. Anything to do with a lifecycle method other than
componentDidMountwill not be run. You can manually pass in updated props and manually rerender the component, but that’s not the same as testing the result of a redux call. For that kind of testing, Cypress may be a better choice.
If you have any links from React Router or want to test that redirects work on your component, RTL’s standard
renderfunction will not be enough. You must use the
renderWithRouterfunction found here. React Router · Testing Library
Named arrow functions in class properties aren’t in the prototype so unit testing them is challenging. A fat-arrow assignment in a class in JS is not a class method; it’s a class property holding a reference to a function.
Snapshots, despite being presented as a first step in mosttseting libraries’ documentation, are not as useful as they seem at first glance and are generally avoided by many developers.
Mocking can be challenging without a full understanding of how it works and what can be expected from it. I’ve written a separate article specifically on mocking covering some of the basics, though it’s important to note that Promises add an additional layer of complexity.
With all that said, documentation is still a solid place to start when learning testing. Reading it through thoroughly and then following up with research into any new terminology will enable you to ask better questions.
I found it helpful to search for any errors that come up as well. Discovering what was a problem with my tests vs. my code vs. a configuration issue was challenging, but with each issue I refined my knowledge of how the pieces fit together.
I also highly recommend joining the communities of the various testing libraries and reading through the issues in their github repos. Many questions I had were answered in these places more efficiently than in the documentation, especially when it came to the complexity added by using Redux, thunk and other non-native React tools.
My own system
Currently, I’ve chosen to use the following setup on my side projects:
- React Testing Library
I also have a “test” folder where all of my test data is stored in fixtures along with a few testing utilities. For reference, the test utilities are stored in a gist. The fixtures and other setup details can be viewed in one of my side projects. (I have a boilerplate in the works that will include this style of testing)
- Fixtures and utilities are stored in the root level
- Use fixtures for data unless it is a small amount (less than line) of single-use data.
- Prefer reusable test data and utilities. Keep tests DRY, too!
- Tests exist alongside the elements they are testing. Each component folder should contain a
__tests__folder and tests should be named to match their component.
Structure of a test file
Not all test suites require all of the structure outline below, but this is the preferred order for each of the files.
- React Testing Library
- Other required packages & libraries
- data fixtures
- connected container component
- unconnected container component
- Mock Functions
- Variables for beforeEach
- beforeEach/afterEach setup/teardown functions
- Describe block
- test block
- variables (including spies)
- test block
Notes on my testing style
- Data fixtures are not aliased to variables unless needed for mocking purposes.
- The first describe block for any test suite is the basic render of the component with the connected store or a test that the function or object being tests exists.
- The second describe block tests basic interactions.
- All other tests come after the first two.
- Multiple expects are acceptable in a single test as long as they are all related to the same functionality and relevant to the test.
I hope some of this information is helpful to those also interested in developing a solid testing strategy. Perhaps one day we’ll have more standardized approaches for testing on the front end, but until then, I encourage you all to share your own findings and experiences working with tests.