Tech, React Native

Component Testing Using React Native Testing Library

With its growing popularity and huge community support, React Native can now ensure proper testing environment and libraries as well.

In this piece we are going to tackle unit testing fundamentals in the React Native app development ecosystem. But first, let's set the record straight with what unit testing is and why it is necessary in any software development lifecycle.

Intro: Unit Testing

In unit testing, each component within a project is tested separately, as a single unit. This allows for testing independent components until they meet the design and functionality specs.

The main idea is to figure out if every component of the software is working as expected. The environment will work efficiently and as intended only when each unit of the software works properly.

As a developer, you can do unit testing, or you can have it performed by independent testers.

Note for testing beginners: Unit tests can be manual as well as automated. The overall process involves creating test cases which are reviewed, reworked on, then executed.

Why Unit Testing is essential

To put it shortly, it's about adopting the test-driven development approach. Test-driven development involves developing and testing each module of a component before it is introduced to the main system, which is a much leaner way of building a system.

What do you gain by using unit testing?

  • Unit tests are implemented as functions. The output of the function is checked by introducing various input values. This makes the debugging process easier.
  • The overall software development process becomes agile: with any new modification introduced, the component can be tested individually and integrated into the main system which is a huge time and cost saver.
  • You reduce the chances of getting annoying bugs at the end as they can be detected and managed during early stages. Overall refactoring and updating becomes much more convenient.
  • Documentation becomes less of a chore, since you have unit testing to provide an in-depth outline for each module and its functionality.
  • The entire code becomes modular. This directly results in reliable and reusable code.
  • It helps to statistically measure performance for each component and a system as a whole while identifying the area a component covers in a system.
  • You avoid delivering error-prone products. Plus, you improve code quality while reducing complexity.

On the whole, unit testing is one of the safest ways to deliver a successful product. At Around25, we've been experiencing this for a while now. Click below to see what our clients think and contact us here if you are building a React Native app.

Getting Started with Unit Testing in React Native

React Native Testing Library offers simple and complete React Native testing utilities.

It is a lightweight solution for testing React Native components and it facilitates utility methods on top of react-test-renderer. This will encourage better testing practices. This library goes hand-in-hand with Jest but can also collaborate with other test runners.

Starter Project

To conduct testing in the React Native project, we need a starter project first. You can go ahead and download the starter project from the snack.

After downloading, we need to run the project on an emulator or a connected device by executing the following command:

Expo start

This gets you a simple React Native app running in your emulator.

In the app, we have an input field, a button to add the item, and the list. The list contains a delete button which deletes the list items. The Home page is divided into three components:

  • Main Home Screen (Home.js)
  • List Component (ItemsList.js)
  • Add Item Component (ItemAdder.js)

To perform the unit test, we need to install the React Native testing library to our project. For that, we need to run the following command in our project terminal:

npm install --save-dev @testing-library/react-native

The library has a peerDependencies listing for react-test-renderer.

For the testers, we are going to use jest here. Thus, we need to install the jest-native library as an additional jest matcher. For that, we need to run the following command in the project terminal:

npm install --save-dev @testing-library/jest-native

Now, we need to configure our jest setup files, patterns in the package.json file. For that, we add the following configuration lines in the jest object present in our package.json file:

{
	"jest": {
		"preset": "react-native",
		"setupFilesAfterEnv": ["@testing-library/jest-native/extend-expect"],
		"transformIgnorePatterns": ["node_modules/(?!(jest-)?react-native|@?react-navigation)"],
		"setupFiles": ["./node_modules/react-native-gesture-handler/jestSetup.js"]
	}
}

We are done with the configurations but did not perform our testing.

So we are creating a new test file in a separate folder that we are going to call ./test inside the ./screens directory of our project. Inside it, we create a file called Home.test.js, which will contain all the tests related to our Home screen and will execute once we run the test commands.

It's time to import the required libraries in the Home.test.js file for the testing purpose:

import React from 'react';
import {render, fireEvent} from '@testing-library/react-native';
import Home from '../Home';

Testing the rendering of the Home screen

To test out the basic home screen template, we render out the Home screen template code into the terminal.

If everything renders properly, great! There is no issue. Now use the render() method and pass the Home component to it.

The render method will provide us with the required methods to test out the inner components of the Home screen.

Likewise, we use of the debug method here to render the Home screen template:

test('rendering Home component', async () => {
    const {debug} = render(<Home/>);	
    debug();
});

At this point we can implement a test function using two parameters, the first parameter being the name of the test and the second being the callback function that triggers when the test is performed.

In the callback function, we can test each element in any component.

In package.json file, we have a script to run the test:

"scripts": {    
    "android": "react-native run-android",    
    "ios": "react-native run-ios",    
    "start": "react-native start",    
    "test": "jest",    
    "lint": "eslint ."
},

Every time we want to execute the test, we need to run the following command in the project terminal:

npm run test -watch

Hence, we will get the full template render of the Home screen template:

In case any error occurs, there may be a problem with the integration of template JSX elements which will be indicated in the error message itself.

Testing the workings of Input Field and Button components

The input field and button component in the Home screen are imported from the ItemAdder component.

To test the TextInput and Button, we need to assign them with testID prop. Using these testID props, we will be able to get hold of these elements in the test configurations.

<TextInput  
value={input}  
onChangeText={setInput}  
placeholder="Add Item..."  
style={styles.input}  
testID={`${testID}-input`}
/>
<Button  
color="#000"  
testID = "input-button"  
title="+ ADD"  
style = {styles.button}  
onPress={() => {    
addItem(input);    
setInput('');  
}}
/>

Now, we can access these input and buttons using the getByTestId method in the Home.test.js file. Then we initialize the input field and button by mentioning their testID as such:

test('testing input and button', async () => {	
  const {getByTestId, getByText} = render(		
     <Home />,	
  );	

  const input = getByTestId('adder-input');	
  const button = getByTestId('input-button');

});

Next, we apply input and trigger the button to click by using the fireEvent object instance imported from the testing library. We can input the text value in input and trigger the pressing of the button with this:

fireEvent.changeText(input, 'element');fireEvent.press(button);

Here, we have provided the input to the TextInput element in our app by using changeText method from the fireEvent class. The input value we provided is 'element'. The press function will trigger the Add button in our app, which will cause the input to be shown in the list.

The question is: how to know if the entered value 'element' has appeared in the list after triggering the press event?

We can check if the text value 'element' is present in the render.

For this, we can make use of the getByText method provided by render(). If 'element' is available in the render then it will be assigned to the element constant. The code is provided in the code snippet below:

const element = getByText('element');

Now, we need to check if the element is present in the render. In other others, we are expecting the element value to be present in the render.

This allows us to use the tobeDefined method from the expect function, which takes the element as a parameter. If the value is present, then no error will occur. Else, there will be an occurrence of error.

expect(element).toBeDefined();

Check the full test function below:

test('testing input and button', async () => {	
  const {getByText, getByTestId} = render(		
    <Home />,	
  );	
  
  const input = getByTestId('adder-input');	
  const button = getByTestId('input-button');	
  fireEvent.changeText(input, 'element');	f
  ireEvent.press(button);	
  const element = getByText('element');	
  expect(element).toBeDefined();
});

Here, we expect our element to be defined when the input is added and to get this test result:

We can see that the test has passed.

Which is why we are now able to check for multiple inputs as well. For that, we assign a different input value 'element1' and do everything just like in the previous test. The function is this:

test('testing input and button', async () => {    

  const {getByText, getByTestId} = render(    
    <Home />,  
  );  
  
  const input = getByTestId('adder-input');  
  const button = getByTestId('input-button');  
  fireEvent.changeText(input, 'element');  
  fireEvent.press(button);  
  const element = getByText('element');  
  expect(element).toBeDefined();  
  
  fireEvent.changeText(input, 'element1');  
  fireEvent.press(button);  
  const element1 = getByText('element1'); 
  expect(element1).toBeDefined();
});

If we run the test command, we get the following result:

Testing Delete Button

In the app demo, you notice that when we add an item to the list we also get a delete button.

When we press it the item disappears. To test the button, we need to assign the testID prop to the delete Button component in the ItemList.js file:

<Button	
  title="Delete"	
  onPress={() => deleteItem(value)}	
  color="#f12"	
  testID="delete-list-item"
/>

Now in Home.test.js, we need to create a new test function called 'testing delete'.

To test the delete button, first we must have an item in the list.

So we are going to input the item using the same coding implementation as before and check for its rendering. Then, we use the getAllByTestID method which fetches every element with the specific testID defined inside it as an array.

Now on to assigning the array to deleteButton constant: we trigger the button using the press method from the fireEvent class as before. But since we are only deleting the first item of the list, we need to assign the array identifier as well. This will cause the first item in the list to be deleted.

So we check if that first item is available in the render or not, by using queryByText. Then, by applying the toBeNull method from the expect function, we can check whether the first item of the array is available or not in the render. Get the function here:

test('testing delete', async () => {    

   const {getByText, getByTestId, getAllByTestId, queryByText} = render(
     <Home />,  
   );  
   
   const input = getByTestId('adder-input');  
   const button = getByTestId('input-button');  
   fireEvent.changeText(input, 'element');  
   fireEvent.press(button);  
   const element = getByText('element');  
   expect(element).toBeDefined();    
   
   const deleteButton = getAllByTestId('delete-list-item');
   fireEvent.press(deleteButton[0]);  
   expect(queryByText('element')).toBeNull();
 });


What we did here is we added the 'element' value to the list, then deleted it and checked if the 'element' is deleted, using the tobeNull method.

If the answer is true, then the element is deleted and the delete function works. The test should also be successful. Now, if we run the test we will get the following result:

As we can see, our testing of the delete button is also successful.

Testing If Item list is present or not after the Input

Now, for the next test we check if the list items are available or not after we input the value and click on the Add button.

test('testing list items', async () => {    

  const {getByText, getByTestId} = render(    
    <Home />,  
  );  
  
  const input = getByTestId('adder-input');  
  const button = getByTestId('input-button'); 
  
  fireEvent.changeText(input, 'element');  
  fireEvent.press(button);  
  const element = getByText('element');  
  expect(element).toBeDefined();  
  
  fireEvent.changeText(input, 'element1');  
  fireEvent.press(button);  
  const element1 = getByText('element1');  
  expect(element1).toBeDefined();    
  
  const listElements = getByTestId("list");
  expect(listElements).toContainElement(element);
});

All the steps are similar except, we have used toContainElement from the expect function to check the specific item we entered in the list is present or not. So we should get the following result:

By preparing different test functions and assigning the test ids to render elements, we can test other components.

Conclusion

Whew, quite a long ride. Let's do a quick recap with what we learned:

  • what unit testing is
  • why it is essential
  • how to perform it in the React Native ecosystem using the React Native Testing library.

And the most important takeaway is that: by writing these types of tests based on component render, we can verify individual components of a React Native app. This will help optimize the app in case any error occurs.

There are other APIs and methods provided by the testing library that you are free to explore. The challenge is to devise your own test for individual components. You can even create a component first, test it out as performed above, and then only integrate it into the core app. This is called Test-Driven Development. The simplicity by which we can perform tests with only a few lines of code is just amazing.

Hopefully, you got the hang of basic component-based unit testing in React Native using the React Native Testing library. If you still have questions, we warmly welcome them in the comments section ☺️. Oh and if you learned something in this article, maybe you want to learn some more by subscribing to our newsletter (check below).

Have an app idea? It’s in good hands with us.

Contact us
Contact us