React Native Testing Library

Topics Covered

Overview

React Native Testing Library is a library that helps you test your React Native components in a way that resembles how users interact with your app. It provides utilities to query the rendered component tree, fire events, wait for changes, and more. It is based on the Testing Library philosophy of testing the user interface rather than the implementation details.

Testing is an essential part of software development, especially for complex applications like React Native apps. Testing helps you ensure that your app works as expected, prevents bugs and regressions, and improves the quality and maintainability of your code.

However, testing React Native components can be challenging, as they involve rendering, state management, navigation, animations, native modules, and other aspects that are not easy to mock or simulate. Moreover, many testing tools and frameworks focus on testing the internal implementation of the components, such as their props, state, methods, and lifecycle hooks. This can lead to brittle and verbose tests that are coupled to the code and do not reflect how users actually use the app.

The problem

Let’s say you have a simple component that renders a button that toggles a text message when clicked:

How would you test this component using a traditional testing tool like Enzyme? You might write something like this:

This test might work, but it has some problems:

  • It relies on the implementation details of the component, such as the names and props of the elements. If you change any of these details, the test will fail even if the functionality remains the same.
  • It uses shallow rendering, which does not render the actual native elements or their children. This means that you cannot test how the component looks or behaves on different devices or platforms.
  • It does not mimic how users interact with the component. Users do not care about props or state; they care about what they see on the screen and what they can do with it.

The solution

React Native Testing Library offers a better way to test your components. It follows the principles of Testing Library, which are:

  • The more your tests resemble the way your software is used, the more confidence they can give you.
  • Avoid testing implementation details; test what users see and do.
  • Be consistent with your environment; use real DOM APIs and events.
  • Keep your tests as simple as possible; avoid setup and teardown logic.

With React Native Testing Library, you can write tests like this:

This test is better because:

  • It does not depend on the implementation details of the component; it only uses what users can see (the role and text of the elements) and do (pressing the button).
  • It uses full rendering, which renders the actual native elements and their children. This means that you can test how the component looks and behaves on different devices or platforms.
  • It uses real DOM APIs and events, which are consistent with the environment and more reliable than simulated ones.

Installation of @testing-library/react-native (using yarn and npm)

To use React Native Testing Library, you need to install it as a dev dependency in your project. You can use either yarn or npm to do that:

You also need to install Jest, which is the recommended testing framework for React Native Testing Library. You can follow the official guide to set up Jest for your project: JestJs

Additional Jest Matchers

React Native Testing Library also provides some additional Jest matchers that can help you write more expressive and readable assertions. For example, you can use toBeDisabled to check if an element is disabled, or toHaveStyle to check if an element has a specific style.

To use these matchers, you need to install another package called @testing-library/jest-native as a dev dependency:

Then, you need to import the matchers in your test file or in a global setup file:

Now you can use the matchers in your tests. For example:

You can find the full list of matchers and their usage here: Jest Matchers

Flow

React Native Testing Library also supports Flow, which is a static type checker for JavaScript. If you use Flow in your project, you can install the type definitions for React Native Testing Library as a dev dependency:

Then, you can import the types in your test file or in a global flow config file:

Now you can use the types in your tests. For example:

You can find more information about Flow here: Flow

Example

To demonstrate how React Native Testing Library works in practice, let’s look at a more complex example of a component that renders a list of items and allows the user to filter them by typing in a search input:

Different Testing Methods

There are different testing methods in React Native, which can be categorized into four levels: unit testing, component testing, integration testing, and end-to-end testing. Here is a brief explanation of each method:

  • Unit testing: Unit testing covers the smallest parts of code, like individual functions or classes. This includes testing JavaScript objects and methods which are present at the component level. The purpose of unit testing is to ensure that each unit of code performs as expected and does not have any errors or bugs.
  • Component testing: Component testing checks how your React components render and interact with the user. This involves testing the user interface (UI) elements, such as buttons, text inputs, images, etc. The purpose of component testing is to verify that your components display correctly and respond to user actions.
  • Integration testing: Integration testing verifies how different parts of your app work together, such as API calls, navigation, data storage, etc. This involves testing the interactions between components and external services or libraries. The purpose of integration testing is to ensure that your app functions properly as a whole and meets the requirements.
  • End-to-end testing: End-to-end testing simulates the real user behavior and scenarios on a device or a simulator/emulator. This involves testing the entire app from start to finish, including launching the app, logging in, performing tasks, etc. The purpose of end-to-end testing is to validate that your app meets the user expectations and provides a good user experience.

These are some of the different testing methods in React Native. You can use various tools and frameworks to implement these methods, such as Jest, Enzyme, Detox, Appium, Cavy, etc.

Advantages and Applications

  • Ensuring the quality and functionality of your app by detecting errors, bugs, and edge cases before they affect the users or the production environment 1.
  • Increasing the confidence in your code by verifying that it works as expected and meets the requirements and specifications.
  • Facilitating the maintenance and refactoring of your code by making it easier to identify and fix issues, as well as preventing regressions and breaking changes.
  • Improving the readability and documentation of your code by expressing the behavior and logic of your components and app in a clear and concise way.
  • Enhancing the collaboration and communication among developers, testers, and stakeholders by establishing a common language and understanding of the app’s features and behavior.
  • Supporting the development process by providing feedback, guidance, and automation for writing, running, and debugging tests.
  • Enabling cross-platform testing by allowing you to test your app on different devices, operating systems, screen sizes, orientations, network conditions, etc.
  • Promoting good testing practices by encouraging you to write tests that resemble the way your app is used by real users, focusing on user interactions and accessibility rather than implementation details 24.
  • Offering a variety of testing types by allowing you to perform different levels of testing such as unit testing, integration testing, end-to-end testing, UI testing, etc.
  • Providing a rich ecosystem of tools and libraries by integrating with other popular testing frameworks, libraries, and utilities such as Jest, Enzyme, React Testing Library, Detox, Appium, etc.

Best Practices

  • Use user events instead of simulating DOM events. User events are actions that users perform on your app, such as clicking, typing, swiping, etc. Simulating DOM events is a low-level approach that may not reflect how users actually interact with your app. User events are more realistic and reliable, and they can also improve the accessibility of your app. For example, React Testing Library provides a userEvent library that you can use to simulate user events.
  • Avoid testing implementation details. Implementation details are the internal workings of your code that users do not see or care about. Testing implementation details can make your tests fragile and hard to maintain, as they may break when you refactor or update your code. Instead, focus on testing the behavior and functionality of your app that users can observe and interact with. For example, React Testing Library encourages you to query the elements by their text content or accessibility labels, rather than by their class names or IDs.
  • Use proper queries to find elements. Queries are methods that help you locate elements in the DOM tree. There are different types of queries, such as getBy, queryBy, findBy, getAllBy, queryAllBy, findAllBy, etc. Each one has a different purpose and behavior. For example, getBy queries will throw an error if the element is not found or if there are multiple matches, while queryBy queries will return null or an array of elements. You should use the appropriate query for your test case and avoid using expect assertions on queryBy queries.
  • Use proper waiting methods to handle asynchronous operations. Waiting methods are functions that help you wait for something to happen before proceeding with your test. There are different types of waiting methods, such as waitFor, waitForElementToBeRemoved, etc. Each one has a different use case and behavior. For example, waitFor will retry running a callback function until it does not throw an error or until a timeout is reached, while waitForElementToBeRemoved will wait for an element to be removed from the DOM. You should use the appropriate waiting method for your test case and avoid using waitFor as a catch-all solution.
  • Use mocks and stubs to isolate your code from external dependencies. Mocks and stubs are fake objects or functions that replace the real ones in your tests. They can help you control the input and output of your code, avoid side effects, and speed up your tests. For example, Jest provides a mock function that you can use to create mocks for modules, functions, or classes 3. You can also use tools like Sinon or Nock to create stubs for network requests or other APIs.

Conclusion

In this article, we learned about React Native Testing Library, a library that helps us test our React Native components in a user-centric way. We saw how it:

  • Allows us to query the rendered component tree by what users can see and do, rather than by implementation details.
  • Uses full rendering, which renders the actual native elements and their children, and allows us to test how the component looks and behaves on different devices or platforms.
  • Uses real DOM APIs and events, which are consistent with the environment and more reliable than simulated ones.
  • Provides additional Jest matchers that make our assertions more expressive and readable.
  • We also learned how to install and use React Native Testing Library in our project, and how to use it with Flow for type checking. We also looked at a practical example of testing a component that renders a list of items and allows the user to filter them by typing in a search input.

We hope that this article has given you a good introduction to React Native Testing Library and inspired you to use it in your own projects. If you want to learn more about React Native Testing Library, you can check out the official documentation here: React Native Testing Library

Happy testing! 😊