Introduction
Have you ever had a bug introduced by solving another problem and didn't realize it until weeks later? This could have been easily avoided if you had tests for your App.
In a project with a tight schedule, tests are generally the first thing to be forgotten and underestimated. Once you understand how to create tests, you'll realize it's not that complicated to add them and avoid future problems.
Overview
In this article, you will learn how to test React Native Apps using jest and @testing-library/react-native.
About @testing-library/react-native
One important thing to point out is that this testing library has changed many times and migrations were needed from one version to another. This tutorial will work with the latest version at the time of writing it ([.c-inline-code]7.0.1[.c-inline-code]). For migrations, you can check their migration guide.
App to be tested
For this tutorial, we are going to use this simple app, but of course, you can apply everything learned in any app you want. In this app, we just have two simple screens with some components we can test.
Configuration
Since we are going to use the [.c-inline-code]@testing-library/react-native[.c-inline-code] library, we need to install it. To use the specific [.c-inline-code]7.0.1[.c-inline-code] version we can install it like this: [.c-inline-code]npm install @testing-library/react-native@7.0.1[.c-inline-code]
We also need to configure jest. We should create a [.c-inline-code]jest.config.js[.c-inline-code] file in the projects root folder and for our example, we can fill it with this content:
CODE: https://gist.github.com/ManuViola77/d98f577c8ea35655d96dcd4654ab398e.js
I will mention some of these configurations later in this tutorial and you can find more information here.
Tests folder
As you can see, our project already has a [.c-inline-code]__tests__[.c-inline-code] folder with an [.c-inline-code]App-test.js[.c-inline-code] file inside. This tests if the app is created and rendered correctly.
Creating tests
To actually create a test, we should create a file with a [.c-inline-code].spec.js[.c-inline-code] extension inside the [.c-inline-code]__tests__[.c-inline-code] folder. We could even add folders inside to better organize the tests, for example, add a folder called [.c-inline-code]screens[.c-inline-code] and have one test per screen inside.
Inside these tests, we are going to use [.c-inline-code]describe()[.c-inline-code] just to describe what we want to test, [.c-inline-code]it()[.c-inline-code] (same as [.c-inline-code]test()[.c-inline-code]) to create the tests we want to have, [.c-inline-code]expect()[.c-inline-code] to assert our expected behavior and [.c-inline-code]beforeEach()[.c-inline-code] to execute some code before each [.c-inline-code]it()[.c-inline-code] test. Here is a complete list of [.c-inline-code]jest[.c-inline-code] methods to use inside tests.
Correspondingly, we use the [.c-inline-code]@testing-library/react-native[.c-inline-code] library to test our components behavior. We could use [.c-inline-code]fireEvent[.c-inline-code] to fire a button pressed event or [.c-inline-code]waitFor()[.c-inline-code] to wait for promises results. Here is a cheat sheet with the possible methods to use.
Notice that to know how to ask for a specific component, we can simply add the [.c-inline-code]testID="componentId"[.c-inline-code] property to the component we want to identify. To simplify, I already added this property to all the components we are testing.
Creating tests for our App
Now we need to create our own tests, so we can test our [.c-inline-code]MainScreen[.c-inline-code] and [.c-inline-code]SecondaryScreen[.c-inline-code]. We can create the [.c-inline-code]screens[.c-inline-code] folder inside the [.c-inline-code]__tests__[.c-inline-code] folder to have better organized tests.
Also, we are going to need some extra files to handle navigation and screen parameters data, so we can create an [.c-inline-code]extras[.c-inline-code] folder inside the [.c-inline-code]__tests__[.c-inline-code] folder for this purpose. If you paid attention you may have noticed that we added [.c-inline-code]modulePathIgnorePatterns: ['extras'][.c-inline-code] to the [.c-inline-code]jest.config.js[.c-inline-code] file. This is to tell jest to ignore the [.c-inline-code].js[.c-inline-code] files that this folder contains, otherwise, it will consider them as tests and we don't want that.
Navigation helper
To test our app we need to start on some screen. If we use a simple [.c-inline-code]render AppStack[.c-inline-code] we would always start on the same screen and we would need to have some flow to go to a different one. To avoid this, we could just start our stack with the screen we want and if we need to test our screen redirecting to some other screen we just need to add that other screen to the stack for the test to work.
Keeping this in mind, we will create a [.c-inline-code]helpers.js[.c-inline-code] file inside the [.c-inline-code]extras[.c-inline-code] folder that looks like this:
CODE: https://gist.github.com/ManuViola77/ebb7e47769a54b2fb6759eeefc912809.js
We will get more into detail when we use it in the [.c-inline-code]SecondaryScreen[.c-inline-code] test.
MainScreen test
Inside the [.c-inline-code]screens[.c-inline-code] folder, we should create the [.c-inline-code]MainScreen.spec.js[.c-inline-code] file.
Since we are in the AppStack's first screen we can just render the entire [.c-inline-code]AppStack[.c-inline-code] and it would start rendering the screen we want.
This file should test the [.c-inline-code]MainScreen[.c-inline-code] components existence and behavior:
- It renders the [.c-inline-code]MainScreen[.c-inline-code] component
- It renders the [.c-inline-code]button-to-secondary-screen[.c-inline-code] component
- When pressing [.c-inline-code]button-to-secondary-screen[.c-inline-code] component, it redirects to the [.c-inline-code]SecondaryScreen[.c-inline-code] component
- It renders the [.c-inline-code]alert-button[.c-inline-code] component
- When pressing [.c-inline-code]alert-button[.c-inline-code] component, it shows an alert
Each of these items should translate to an [.c-inline-code]it()[.c-inline-code] jest method and so our [.c-inline-code]MainScreen.spec.js[.c-inline-code] file should look like this:
CODE: https://gist.github.com/ManuViola77/809d28c61fdad272d2ce1b3d3147bde5.js
SecondaryScreen test
Inside the [.c-inline-code]screens[.c-inline-code] folder we should create the [.c-inline-code]SecondaryScreen.spec.js[.c-inline-code] file.
Since we are not in the AppStack's first screen and our screen receives parameters, we are going to take advantage of the helper we created. We will call the function [.c-inline-code]renderWithNavigation[.c-inline-code] with these parameters:
- [.c-inline-code]mainComponent[.c-inline-code]: [.c-inline-code]SecondaryScreen[.c-inline-code]
- [.c-inline-code]otherComponents[.c-inline-code]: [.c-inline-code][{name: 'MainScreen', component: MainScreen}][.c-inline-code]
- [.c-inline-code]screenConfig[.c-inline-code]: [.c-inline-code]{initialParams: { screenParameters }}[.c-inline-code]
We will create a [.c-inline-code]data.js[.c-inline-code] file inside the [.c-inline-code]extras[.c-inline-code] folders to set our [.c-inline-code]screenParameters[.c-inline-code]:
CODE: https://gist.github.com/ManuViola77/2871545d426cdda330f322e517c46b0e.js
This file should test the [.c-inline-code]SecondaryScreen[.c-inline-code] components existence and behavior:
- It renders the [.c-inline-code]SecondaryScreen[.c-inline-code] component
- It renders the [.c-inline-code]back-button[.c-inline-code] component
- When pressing [.c-inline-code]back-button[.c-inline-code] component, it redirects to the [.c-inline-code]MainScreen[.c-inline-code] component
- It renders the [.c-inline-code]title[.c-inline-code] component
- It renders the [.c-inline-code]param-one[.c-inline-code] component
- It renders the [.c-inline-code]param-two-content[.c-inline-code] component
Notice that our [.c-inline-code]param-one[.c-inline-code] and [.c-inline-code]param-two-content[.c-inline-code] only render if they have content to render, so if our tests pass means that the screen is getting the parameters right!
Our [.c-inline-code]SecondaryScreen.spec.js[.c-inline-code] file should look like this:
CODE: https://gist.github.com/ManuViola77/84c055e49f70d48423d5db302bb2fefd.js
Running tests
To run the tests we just simply need to execute [.c-inline-code]npm test[.c-inline-code]. This command will run all the tests we have under the [.c-inline-code]__tests__[.c-inline-code] folder. If we want to just run one particular test, we can do [.c-inline-code]npm test __tests__/screens/one_particular_test.js[.c-inline-code] instead.
We can see how our tests run successfully:
Debugging tests
If we want to debug our tests we can use ndb.
To use this, first, we need to install it, as the documentation explains, by running [.c-inline-code]npm install ndb[.c-inline-code] and you can add the [.c-inline-code]-g[.c-inline-code] option to make it global.
Then, we can configure our project by setting the value [.c-inline-code]"test:debug": "ndb jest",[.c-inline-code] inside our [.c-inline-code]package.json[.c-inline-code] file under the [.c-inline-code]"scripts"[.c-inline-code] section (we can put it right after the [.c-inline-code]"test"[.c-inline-code] value).
Finally, we can debug our tests by running [.c-inline-code]npm run test:debug[.c-inline-code] and this will install Chromium where you will be able to set breakpoints in your tests and debug them.
Mocks
Since our app was really simple, we didn't need to implement mocks, but it's really an important part of testing react native apps. I will explain why we need them and how we can create them.
The first thing you need to know is that there are lots of functionalities you will have to mock in order to use and test. For this purpose, you can create a [.c-inline-code]__mocks__[.c-inline-code] folder inside the [.c-inline-code]__tests__[.c-inline-code] one. Every file in there will be named after a react native library, so when you are testing something that uses that library, jest will take and use the implementation made in the mock.
Let's exemplify. Suppose that in our project we have a backend we access through a URL that we have saved in an [.c-inline-code].env[.c-inline-code] file with the name [.c-inline-code]API_URL[.c-inline-code] and we use the library [.c-inline-code]react-native-config[.c-inline-code] to access that information. Our tests won't be able to access that [.c-inline-code].env[.c-inline-code] file, so instead, we have to mock it. We would create (if we haven't already) the [.c-inline-code]__mocks__[.c-inline-code] folder and inside we would create a file named [.c-inline-code]react-native-config.js[.c-inline-code] that contains the URL we want, like this:
CODE: https://gist.github.com/ManuViola77/2847c82b2dd3f7e8a8ffbe015735c63e.js
A slightly more complex example would be if we use an [.c-inline-code]ImagePicker[.c-inline-code] to access our photos gallery. Let's say we use the [.c-inline-code]react-native-image-crop-picker[.c-inline-code] library and in our code we call [.c-inline-code]ImagePicker.openPicker()[.c-inline-code] expecting a selected image as a result. If our test calls the button that opens the picker expecting a selected image as a result but we don't have the picker mocked, our test would fail, getting a null result, which is not what we expected. So, we have to create our [.c-inline-code]react-native-image-crop-picker.js[.c-inline-code] file and inside we could have something like this:
CODE: https://gist.github.com/ManuViola77/ca176e397e8b013535ac892bc928034c.js
What we are doing here is mocking our [.c-inline-code]openPicker[.c-inline-code] function, by saying it is a function ([.c-inline-code]jest.fn()[.c-inline-code]) and that its mocked implementation would be [.c-inline-code]Promise.resolve(result)[.c-inline-code]. Notice that since our real [.c-inline-code]openPicker[.c-inline-code] is a promise, our mocked implementation is also a promise. We can decide if we want our mocked promise to be completed successfully (with [.c-inline-code]Promise.resolve()[.c-inline-code]) or if we want it to fail (with [.c-inline-code]Promise.reject()[.c-inline-code]). We also decide if we want to return something, like in this case, we are returning [.c-inline-code]result[.c-inline-code]. A question you might be asking is, how do I test both fail and success results by only mocking one [.c-inline-code]openPicker[.c-inline-code] function? Well, the answer is quite simple actually, and it would look like this:
CODE: https://gist.github.com/ManuViola77/00899b450baebb8ef676c69a31af1b67.js
By using the method [.c-inline-code]mockImplementationOnce()[.c-inline-code] we can give different behaviors to our function, depending on each time you call it. So in this case, the first time we call it we would get a rejected promise (for example to mock that we don't have access to the gallery yet), the second time would be a successful case with our expected result and if we were to call it more times it would use the default implementation inside the [.c-inline-code]fn()[.c-inline-code] method, which in this case would also be successful.
Summary
In this tutorial, we saw how to use [.c-inline-code]jest[.c-inline-code] and [.c-inline-code]@testing-library/react-native[.c-inline-code] to test React Native Apps. You can find the complete GitHub project (with tests included) here.