Testing React - an overview

March 08, 2019

Testing your frontend application has never been easier before. Within this article, I’ll explain different levels of testing your application as well as the most suited solutions for testing the specified levels. Also, there is a repository located at https://gitlab.com/takethefake/react-testing-todo where all best practices are used.

What do we need to keep in mind when testing

When you start with testing an application you probably know or have an idea on what you want to achieve with testing but don’t know how to get started. Which mindsets you need to follow to keep on track. As I started testing I felt literally the same and didn’t know where to start and even more important when I need to stop testing. That’s why I took a look around for some principles that have been developed over time and found the following principles of software testing which since then became a mindset I followed.

The seven principles of software testing

Testing shows the presence of defects Testing can only assure you have bugs in your application, it can’t prove your application is error-free.

Exhaustive testing is impossible You can’t cover each possible scenario, focus on the most important aspects to test.

Early testing The sooner you start testing during an iteration or in your whole project lifetime the better you can direct your development flow. Earlier found bugs are cheaper to fix than those found later.

Defect clustering Most of the reported defects will occur in clustered regions within your code e.g. 80% of the problems are found in 20% of the modules.

The pesticide paradox Running the same tests over time will not detect new defects in your application. As your applications evolve your tests need to evolve too.

Testing is context dependent Testing can happen with different focus depending on your application. A medical application needs to be flawless whereby a professional website has to be performant.

The absence of errors fallacy Related to the first principle the absence of errors does not mean there are no errors that will occur in a shipped version

The stuff your tests are based on

To run your tests you need a few tools to get started and create an environment your tests are able to run inside.

Unit Testing Framework

First of all, you need a unit testing framework. A unit testing framework defines how your tests should be structured, collects all tests in runtime and defines the way your tests are executed.

A testing framework is also able to generate an overview of the code coverage your tests achieved during a test run. Code coverage describes how many lines of code in a file or in total are covered by the executed tests. This gives you an indicator of whether you have written enough tests to cover your application. But beware, high code coverage does not imply you have developed a low-error application. Code coverage is only that relevant when you have written meaningful tests.

The structure of a test has become a kind of standardized:

This is the structure that the testing frameworks Jest, Mocha and Jasemine use as a guideline.

Mocha runs on Node.js and in the browser. Mocha performs asynchronous Testing in a simpler way. Provides accuracy and flexibility in reporting. Provides tremendous support of rich features such as test-specific timeouts, JavaScript APIs etc.

Jasmine: Jasmine is the behavior-driven development framework for JavaScript unit testing. It is used for testing both synchronous and asynchronous JavaScript Code. It does not require DOM and comes with the easy syntax that can be written for any test.

Jest: Jest is used by Facebook so far to test all of the JavaScript code and is based on Jasmine. It provides the ‘zero-configuration’ testing experience. Supports independent and non-interrupting running test without any conflict and does not require any other setup configuration and libraries. Because Jest is by Facebook it has also a very good integration when working with React.

A more detailed comparison between those testing frameworks can be found here.

Assertion Library

Now that you are able to run your tests you need a way to describe what you want to test, and how to categorize a successful outcome. To achieve this you need an assertion library, which tests if your expectation on your unit under test is correct.

Chai is the assertion library that is commonly used with mocha and similar to Node’s built-in assert. When testing with Chai you can choose between three kinds of assertion assert, expect or shouldwhich behave equally in general. An example of those different assertions styles can be found in Chais’ documentation.

Jest contains an assertion library as well which is based on the expect assertion. Expect is used similarly to the way it is handled in Chai. Besides that you will rarely call expect by itself. Instead, you will use expect along with a “matcher” function to assert something about a specified value.

The best way to understand expect is with an example. You want to test a function that returns a specific string on call. You would test it with Jest like this:

In the above example toBe is the matcher of the expect-function. The test will pass if the function really returns blue as string otherwise it will fail.

A more detailed overview of the expect-function in Jest can be found in the Jest docs.

Conclusion the stuff your tests are based on Since I’m primarily working with React I truly feel at home with Jest, because it’s easy to set up and has great support for React. Besides that you are also able to test your application very well with Mocha and Chai but this needs a little more work on your configuration of those both libraries. In general I don’t think there is much of a difference between both but I went for Jest because it’s also from Facebook like React and I think there will be better support for Jest than for Mocha whatever may come. A more detailed comparison between Jest and Mocha can be found here.

Different levels of testing

Since you now have a basic understanding of the environment our tests are running in, you may ask what to test and especially how to test. When talking about testing you will notice that there a three separate levels of testing: Unit, Integration, and End to End(E2E) tests.

The difference between those kinds of tests is mainly the amount of code that is covered by a single test. While you focus on the validity of a single component or function in a unit test. You want to ensure the manner of function between certain components/function in an integration test till you test a certain behavior of your system while imitating the actions a possible user may trigger in your system with an E2E test.

As you may expect it is a lot easier to write a unit test instead of an E2E test, and therefore it’s cheaper to write Unit tests. Also, E2E tests need more processing time to evaluate that a test is passing than a Unit test. But also and this is crucial: you will also gain a lot more confidence from E2E tests than from Unit tests. And this is literally all that we want to achieve from our tests:

Being confident everything works as expected.

Distribution of your effort to test the different levels

You may know the testing pyramid? Its shape defines the number of tests that should be written. As you are moving up on the pyramid the tests are getting larger and less frequent. Developers at Google suggest 70/20/10 distribution, 70% unit test, 20% integration and 10% end-to-end test.

testing pyramid from [Kent C. Dodds](undefined)’ slidestesting pyramid from Kent C. Dodds’ slides

But as I explained before, Unit tests don’t give us that much confidence that our app behaves exactly as we want it too. Kent C. Dodds proposed the Testing Trophy instead in the article “Write tests. Not too many. Mostly integration.” where he explained that you should focus on writing integration tests because they are a good balance between effort and confidence increase. Also, he introduced static code analysis as a possibility to test your code.

Static code analysis

Static code analysis is a method to debug your application without running it. In the process, the structure of your code is inspected and you will receive warnings or errors if your code contains certain discrepancies and therefore will not work as you expect it to. That’s why static code analysis is relevant for testing your code and creates the first layer of your testing experience.

ESLint

With ESLint you are able to validate your code against certain patterns. For example, if you have used undefined variables or assigned a value to a variable and have not used it after the assignment. You can configure a wide range of different rulesets within your eslintrc extend existing rulesets and define your own error level if misbehavior occurs. To get an overview of how ESlint works check the demo on their site.

If you have started your React project with create-react-app ESLint is already properly defined. If you want to adjust your ESLint settings you either have to eject your application, which I would not advise you to do, or use craco by sharegate. CRACO gives you the possibility to extend the existing React configurations for Webpack, Babel and ESLint to your needs and maintain the possibility to upgrade your react-scripts aswell.

It is possible to enforce formatting rules with ESLint but you should avoid this. ESLint should be used to verify the structure of your code. To verify code style use prettier which reformats your code with ease to the defined coding style standard.

Prettier

Prettier’s main task is to prettify your source code based on rules defined in a config which is applied to your codebase, so your code style is equal throughout your project. This comes in handy, especially when working with coworkers on the same codebase.

This is cool, but why is prettier relevant for static code analysis of your code, you may ask? Prettier prettifies your code by parsing and reprinting your code based on your configuration. What means that if prettier isn’t able to prettify your code you likely have made a syntax mistake, like missing brackets, in your code. If you run Prettier on save (check out this extension for prettier in vscode) you are able to identify those typos with ease. Also, a uniform code style throughout your code will give you a better understanding of what is happing in a specific file. Care about your code’s function not about its style.

Flow

Flow is a static type checker by Facebook for JavaScript which adds a certain amount of type safety to your JavaScript code which can be tried out at their demo. If you defined your types properly flow will hint you, when you have a conflict between the types you expect and the types you pass with your variables.

The following function is taken from the flow demo. Each file that is relevant to flow is annotated with an /@flow / at the start of the file. The annotation is needed for flow to get all relevant files for type checking.

If you run flow on this code, the following error will occur

    4:     return x;
                  ^ Cannot return `x` because number [1] is incompatible with string [2].
    References:
    2: function foo(x: ?number): string {
                        ^ [1]
    2: function foo(x: ?number): string {
                                 ^ [2]

Which hints the developer to not return a number if a string is expected.

Because flow is a productivity tool you can simply add it to your dependencies to start typing. Even in a large project that has no type-support yet. This allows you to gradually add a little bit of type safety

In addition, Flow also provides the backbone needed for many useful IDE features such as Error Highlighting, Autocomplete, and Automated Refactoring. With this, even regular text editors like Atom and Visual Studio Code can be configured to support these features for JavaScript.

Typescript

Typescript is a strict synthetical superset of JavaScript which is developed by Microsoft. It provides an own compiler which compiles Typescript code to JavaScript code. Similar to Flow it contains a static type checker that validates the code during compilation time and while developing.

Almost identical function than before

will throw the following error when being compiled by typescript

    tsc.ts:3:5 - error TS2322: Type 'number' is not assignable to type 'string'.

    3     return x;
          ~~~~~~~~~

Typescript can’t be added as simply as flow because your code needs to be compiled by the typescript compiler tsc. So you have to migrate your codebase to be typescript compatible to enjoy typescripts static code analysis in your codebase.

The IDE integration for Typescript feels a lot faster than with flow and the autocompletion for example in VSCode is a lot more helpful. A more detailed comparison between Flow and Typescript can be found here.

Besides Typescript, there are even more languages that provide type safety and compile to JavaScript. I will not cover those here and just drop a reference for you to dig deeper. To name a few: Reason, Elm or Dart.

Conclusion static code analysis I’m using ESLint and Prettier in almost all of my projects because once configured it just does its job of providing me a good hint what I’m doing wrong while developing. Flow is my candidate when you already have a big codebase that has no defined types because it’s easy to just start typing with Flow. When I start a big project from ground up I am using Typescript most of the time because of its good typing integration in many IDEs.

Unit tests

When writing unit tests, you are taking an individual component and isolate it from other components to test its behavior. You can unit test the outcome of functions or the way a component renders when passing different arguments.

An easy function to test would be the following a sum function which could be tested with this easy test

This is a totally straightforward way of testing functions. Bit in general we want to achieve confidence in our components.

Render and test React components

When it comes to React components you want to check how your component is rendered and if all props you pass to the component influence the behavior of your component as expected. That’s why we need a possibility to render our components within Jest.

Jest can use two different environments under the hood:

  • jsdom: which emulates a browser environment(e.g. document/window is declared and useable) in javascript

  • node: plain node environment especially suited for backend or functional tests without any UI-elements.

Since we want to test our React components we go with jsdom.

The example component we want to test is a simple input component which can be seen in the code sandbox.

I want to unit test this component in three different ways. One of them might be already very familiar to you.

React DOM You are already using React DOM for rendering your whole application so it is able to render certain components as well to test them.

In the example above we are creating a new div inside the document which is used as a container and render our unit under test inside it. Now that the component is rendered we can check if the components fulfill all necessary properties. For example, a prop that is rendered at a specific position.

Since this process of rendering and selecting may become a bit tedious I’d like to present you two alternatives “Enzyme” and “React Testing Library”.

EnzymeEnzyme is a JavaScript testing utility for React from AirbnbEng that makes it easier to assert, manipulate, and traverse your React Components’ output.

Besides the rendering with mount and selection of elements with find enzyme is also able to call specific functions from your components outside of the scope. Enzyme enables you to really deep dive inside the functionality of your component to check each bit that may be necessary for your application to run properly. You are able to check the state and props of your components and in general, I haven’t found an interaction you are not able to trigger or get with Enzyme. The whole spectrum of Enzymes functionality can be found in their docs.

A more detailed overview of enzyme and how to set it up within your application can be found in the following article by Dominic Fraser. [Testing React with Jest and EnzymeThis post will look at how to setup and use Jest and Enzyme to test a React application created with Create React App…*medium.com](https://medium.com/codeclan/testing-react-with-jest-and-enzyme-20505fec4675)

React Testing Library React Testing Library is a library that uses Dom Testing Library under the hood and is written by Kent C. Dodds as he prepared a course on the basis of Enzyme. The main reason why he wrote it was because of his opinion that you get confidence from integration tests and especially by interacting with a component like a user would do. An explanation of what React Testing Library is can be found in his article where he introduced it. The approach of testing an application like a real user would use it makes it hard to test implementation details. When testing implementation details your test will become flaky and break often because they depend too much on your implementation. You should definitely check out this library since it is recommended by React as well in their docs. A few examples on how to write tests with react testing library can be found in a specified code sandbox.

Mocking

Since you need to decouple your unit under test from other components/module dependencies to test them in isolation you need a possibility to mock them away. Jest provides two separate ways for mocking.

  • mocking a function

  • mocking a module

You can create a mock function with jest.fn() and pass an alternate implementation as an argument. This function can be used instead of the original one, for example when assigning a click handler via props. After the execution of the unit under test is finished you can inspect how it interacted with the mock function by checking the .mock property of the function which contains an array of calls the corresponding arguments and results which were returned during the execution phase.

Using mock functions becomes therefore very handy to validate your component interfaces against other components or services.

When using some third party libraries like fetch or axios for server communication, you can mock away the server part as well by using jests mock module functionality. When mocking a module you would use jest.mock(). Then you can provide custom callbacks or implementations for each function which is defined inside the mocked module.

Jests mocking functionality is described in more detail in the Jest docs. Also, take a look at Kent C. Dodds article about mocking. The Merits of MockingWhat are you doing when you mock something, and when is it worth the cost?blog.kentcdodds.com

Snapshots

Another neat feature Jest provides is the possibility to snapshot a certain state of your application. During each test run your previous snapshot will be compared with the current snapshot. If a difference occurs the snapshot test will fail. Snapshot tests are useful if you do not want your UI or certain function results to change unexpectedly.

A basic snapshot test can look like the one below:

The actual test is defined from line 5 to 10 but after the first execution of the test, a new folder snapshots is being created aside from the tests which contain text files with the respective snapshots of the tests. From now on each test run will compare both outcomes.

One pitfall that might occur with this approach is that you are snapshotting really large objects or DOM-nodes without noticing. You want to avoid large snapshot’s because the larger a snapshot gets the higher is the possibility for it to fail. There are two solutions to overcome this uncertainty.

To use inline-snapshots, just replacetoMatchSnapshot with the toMatchInlineSnapshot Method.

Now the output of the snapshot is directly written inside the test it affects, which gives you a good overview of what is covered by the test.

Sometimes you know that certain properties will change e.g. in case of a timestamp or an id. In this case, you can use property-matcher to just validate the type of certain properties and not the values itself.

Do not snapshot everything just because it is that simple. If you do not keep an eye on the size of the snapshots it is likely that those will fail. Also, it is very easy to update failed snapshots within jest. So it might happen, that you or a teammate just updates the snapshot without validating the correctness. If something like this happens your snapshots will become trivial. Treat your snapshots like you treat code, commit and review them! Effective Snapshot TestingSnapshot testing can be useless, or super useful. Your choice. Let’s talk about how to make them useful.blog.kentcdodds.com

Conclusion unit tests Unit tests have a fast execution time and are not that hard to write. If done correctly they are very reliable in verifying that certain units does not have errors. Also, when unit tests fail the tests that fail will give you a very good hint where to check your code because they only test isolated parts. When writing unit tests you have to keep in mind that you don’t want to test certain implementation details. Since the implementation may change over time and you want to achieve exactly the same behavior you had before so you do not want to modify your tests. Sometimes it is quite hard to spot the difference of a feature and an implementate detail. Also, you have to keep in mind that your test results are valid in the isolated test environment and it is not proven that your components will interact well with each other. In my opinion unit tests can be very effective when testing a certain edge case but i would not focus most of my effort in writing unit tests i would rather rely on integration tests.

Integration tests

An integration test is a test which tests a group of several components and their interaction with each other. In contrast to unit tests which should ideally just test one component at a time. Sometimes it can be difficult to differentiate between a Unit or an Integration test, especially when testing components which consist of other components.

The tools you use for integration testing are the same as you use for Unit testing. The difference starts when you decide on what to render and test. In general, the best thing to do when writing integration tests is to stop mocking other components or functions. Also, render every component which is defined on a global level throughout your application, e.g. react-router or redux. If one of your components uses one of those libraries, eventually it would call a function of those libraries during execution. In this case, you want to validate whether the integration works out as planned.

Mocking server communication So in general with an integration test, you literally test the integration between certain components within your frontend. Also, you want to be sure to mock all requests to a possible backend or third party with the expected output the request would result with.

When writing integration tests, try to reassemble the way the users are using your components and try to interact the same way with them as the users do. Also, don’t just test sunshine cases, what could possibly go wrong when using the component? Use the integration tests to harden your code and deliver great value to your customer.

If you want to dig deeper on why it is so important to focus on integration tests, check out this article by Kent C. Dodds. Write tests. Not too many. Mostly integration.Guillermo Rauch tweeted this a while back. Let’s take a quick dive into what it means.blog.kentcdodds.com

Conclusion integration tests Integration tests strike a great balance on the trade-offs between confidence and speed/expense. This is why it’s advisable to spend most (not all, mind you) of your effort there.

End to end tests

An end to end test is a test which tests your whole application and where you only mock away third-party applications. This gives you the possibility to test user stories and therefore the expected behavior of the application a possible user would experience. You could use the previous tools for e2e testing as well by mounting the root component and interact with it, but there are a few more suited possibilities out there: TestCafe and Cypress. Both do not rely on selenium and define a bundled all in one solution for end to end testing.

TestCafe TestCafe started as a commercial solution in 2013 but was rewritten and open-sourced in 2016. It is based on an own implementation of a test runner and therefore introduces some sort of new patterns to test your code. The attractive parts of TestCafe are:

  • Cross-browser support:* *TestCafe offers support for most of the modern browsers, along with testing on cloud testing platforms, such as BrowserStack and SauceLabs.

  • Support for native browser events:* *Events such as file uploads were supported.

  • Parallelization of test execution:* *TestCafe supports parallel test execution in most modern browsers, which drastically decreases the runtime of tests in a given environment.

Even if this sounds really nice, there are some downsides when using TestCafe:

  • Documentation:* *It is not obvious how to achieve a certain behavior with TestCafe. The Documentation does not introduce you that well into how to use TestCafe best in your scenario and even the community seems to struggle using it.

  • Code Structuring with PageModel: * *Selectors to certain elements on your page as well as fixtures should be saved in separate files which define the structure of a page. The separation of concerns feels unnecessary and it’s quite confusing when writing tests this way for integration and unit tests and in a different way for e2e tests.

  • No user-centric approach like react-testing-library: Since our integration tests are based on a user-centric approach we don’t want to shift the focus when using TestCafe which does not provide an extension library like react-testing-library.

Cypress.io Cypress is an end-to-end framework that was created by Brian Mann, who wanted to solve some pain points that a lot of developers face when writing integration tests: hard to write, unreliable and too slow. Unlike TestCafe, the assertion libraries used were ones that most developers are used to working with: Mocha and Chai, which lends itself well with our current setup of assertion libraries, using describe() and it()blocks. Some other features that resounded with us were:

  • Relatively mature community:* *There are companies who are already using Cypress for integration testing, such as PayPal. Searching on the web for resources is quite easy and there is an actively worked on a roadmap to support issues and features that the community has reported or requested for.

  • Great documentation:* *API documentation and changelog are both actively updated with each release.

  • Easy to debug:* *Debugging integration tests can be painful, but not in Cypress. Whether running tests in a headless or non-headless state, it’s easy to debug the code through the output in CLI (headless) or in Chrome DevTools (non-headless).

We also noted that there were a few drawbacks of Cypress:

  • No cross-browser support: There is currently no cross-browser support for cypress, but it is currently in development. The reasons why cypress did not focus on cross-browser support are listed here and the current progress can be seen here.

  • Native browser events not supported:* *Currently, native browser events are not supported in Cypress, which includes file uploads. This has an impact of us being able to properly test file uploads for our Create Set page, but there’s currently a proposal that’s being worked into the roadmap to support this.

https://docs.cypress.io/examples/examples/tutorials.html

Conclusion end-to-end tests We started E2E testing with testcafe and gained some first impressions with it, but the page model as well as the lack of good documentation made it easy for us to transition to cypress. Since we are using cypress it became a lot easier to write e2e tests and it became that simple, that it is possible for our testers who do have a basic level of coding skills are able to write e2e tests based on a certain user story to validate the correctnes. Don’t test the same code through ui again — interact directly with the server and set necessary attributes on the client for example with POST-Request to setup your test scenario.

Conclusion

We are almost done, let’s recap on what we have learned throughout the article. We have learned about the different testing paradigms which you should always keep in mind when writing tests for an application as a guideline. Also, we covered different unit testing frameworks and assertion libraries which are necessary to run your tests at all. We learned about the different levels of testing Unit/Integration and End to End and how we should distribute our effort among these kinds of tests.

A general best practice you should keep in mind is that you should test your application behavior, not the implementation of the behavior. react-testing-library and cypress-testing-library support you on doing so by making it hard for you to test the underlying implementation. Implementation can change but behavior doesn’t change that fast. By testing behavior, you won’t have to adjust your after each minor code changes.

Do not get too crazy about writing tests! Ensure a steady coverage increase with meaningful tests but don’t try to achieve 100% Coverage. Identify critical business cases and ensure those work as expected.

To sum up, it is all about confidence! Testing should help you to be confident to press the release button without the fear that something critical possibly could fail and your phone is about to ring with an angry customer at the end of the line.

Be confident, start testing!

References

© 2023 Daniel Schulz       Datenschutz