Editor’s note: This article was last updated on 9 August 2023 to provide more information about snapshot tests and mocking external API requests.
React Native is among the most commonly used libraries for building mobile apps today. In this guide, we will explore unit testing in React Native applications, covering benefits, best practices, testing frameworks, and more.
Jump ahead:
Unit testing is the practice of testing small, isolated pieces of code, including functions, classes, and components. We can test each of these individually to ensure they work.
Consider the example below, a snippet from a utils.js
file in a JavaScript project:
// utils.js function toJSON() {...} function convertToSLAs(args) {...} function hourToMins(hour) {...} export {toJSON, convertToSLAs, hourToMins}
The file contains functions that perform different actions. We can unit test each function in this file, meaning we can test each of the functions independently across different conditions to make sure they each work as intended:
// test/_tests_.js describe("toJSON tests", () => { it("should return object", () => { expect(toJSON(..)).toBe(...) ... }); ... }); describe("convertToSLAs tests", () => { it("should return true", () => { expect(convertToSLAs(...)).toBe(true) ... }); ... }); describe("hourtoMins", () => { it("should be truthy", () => { expect(hourToMins(...)).toBe(true) ... }); ... });
As you can see, we wrote tests for each function. Each test suite contains a number of tests that completely cover the work of the function.
Testing frameworks like Jest, Mocha, Jasmine, and others provide detailed results from the tests. Any tests that fail the conditions are marked as failed, indicating they’re in need of further examination; of course, the tests that pass are marked as passed.
Think of a failed test as a good thing. When a test fails, it usually means something isn’t right, which gives us an opportunity to fix the problem before it impacts our users. Unit tests are great for giving quick feedback and providing confidence that our functions will work as intended in production.
Code written with testing in mind usually adheres to best coding practices as it tends to be loosely coupled and more modular, which improves overall code quality. Writing unit tests encourages developers to write modular and well-organized code.
Unit testing checks specific parts of the code and enables developers to identify and fix errors before they become more significant or before the code is deployed. In turn, this makes the codebase more maintainable and ensures that it is of high quality.
Unit tests are a great way to document as they serve as living documentation for your code. Unit tests provide concrete examples of how your code is supposed to work and are useful for other developers who may need to go through your code for whatever reason. This documentation is valuable for when other developers need to understand and work with your code or when you revisit your code after some time.
With unit tests, bugs are found early in the software development process. This makes it easier to fix the bugs or problems and prevents these issues from moving to the production or release stage of the development process. This saves the amount of time that would have been spent on debugging.
Unit testing enables developers to write reliable code by identifying and resolving bugs early. This maintenance can be done by anyone who will quickly understand the code thanks to the existence of the unit tests.
Unit tests focus on testing individual components or units of code in isolation. You can use testing libraries like Jest, Enzyme, or React Testing Library. Unit tests help ensure that each component behaves as expected and that specific functions work correctly.
Integration tests provide a more holistic view of system behavior as they focus on testing the interactions and integrations between different components or modules of a system to ensure that they work together as intended. They verify the interactions between various components and modules within the app, ensuring that the integrated parts of the app work well together. Integration tests for React Native can be written using testing frameworks like Detox.
Snapshot testing is a powerful testing method used in React Native and in other Javascript-based frameworks to ensure the consistency of UI components on different renders over time. It typically involves taking a snapshot of a rendered UI component and saving it as a reference point. At a later time, the test is rerun, and the new output is compared with the previously saved snapshot. The stored snapshot is usually how the UI should look according to the developer’s expectations. If there are any differences between the two, the test will fail, indicating that the UI has changed unexpectedly.
Here’s the process flow for snapshot testing:
react-test-renderer
or @testing-library/react-native
react-test-renderer
. This utility renders the component into a virtual DOM and provides you with methods to interact with and inspect the rendered output. When you run the test for the first time, a snapshot of the rendered component’s output is generated and saved in a designated file--updateSnapshot
or -u
. This overwrites the existing snapshot with the new outputE2E testing involves testing the entire application workflow, mainly from a user’s perspective, to ensure all app parts work together correctly, including the UI and navigation. It simulates real user scenarios and aims to validate that the entire application behaves correctly, including its user interface and underlying functionality. Tools like Detox and Appium can be used for E2E testing in React Native.
Mocking external API requests in React Native tests involves simulating the behavior of actual API calls without making genuine network requests. This enables you to test components or features in isolation, control the data and scenarios you want to test, and eliminate dependencies on external services.
To mock API requests, you first need a mocking approach. You can decide to create your mock functions using JavaScript’s built-in mocking features, or you can use a mocking library. There are several mocking libraries that can be used in React Native, including:
Tests should be readable and maintainable. The test should be short and test one condition at a time. Consider the test below, based on a function it
:
it("should be truthy", () => { expect(hourToMins(3)).toBe(180); expect(hourToMins(5)).toBe(300); expect(hourToMins(null)).toBe(null); });
The test above has multiple conditions in just a single test, which makes it ambiguous:
it
returns 180 minutesit
returns 300 minutesnull
is passed, it
returns null
Instead, each condition above should be a separate test:
it("should return 180", () => { expect(hourToMins(3)).toBe(180); }); it("should return 300", () => { expect(hourToMins(5)).toBe(300); }); it("should return null", () => { expect(hourToMins(null)).toBe(null); });
This makes it much clearer. Tests should also be as descriptive as possible. In the above tests, the descriptions aren’t clear; the description should be clear enough to convey what is being tested against a condition.
The React Native docs provide some best practices for testing:
Do your best to cover the following:
1. Given — some precondition.
2. When — some action executed by the function that you’re testing.
3. Then — the expected outcome.
This is also known as AAA (Arrange, Act, Assert).
So let’s make our tests more descriptive:
it("given 3 hours, hourToMins should return 180", () => { expect(hourToMins(3)).toBe(180); }); it("given null, hourToMins should return null", () => { expect(hourToMins(null)).toBe(null); });
There we go — much clearer!
There are many testing frameworks we can use to test React Native applications, including but not limited to Mocha, Jest, Jasmine, Nightmare, and WebDriver.
All of these are good choices for testing JS-based applications. Jest and Enzyme are particularly good, and both come highly recommended for testing React-based applications. But because React Native isn’t for web applications, neither Jest nor Enzyme has a React Native adapter.
Luckily, there are a few libraries that can help us out with unit testing for React Native applications, including React Test Renderer and React Native Testing Library.
Jest provides the testing environment, and React Native Testing Library provides a lightweight solution for testing React Native components. React Test Renderer provides a React renderer we can use to render React components to pure JavaScript objects without depending on the DOM or a native mobile environment.
Unit testing in React Native also covers component tests. Components are fundamental units of any React Native app; each component renders its own section of the app, and users directly interact with its output. There are two things we test in components:
From here, we will explore how to add testing functionality to our React Native app. First, let’s scaffold a new React Native project by installing the expo-cli
tool globally:
yarn global add expo-cli
Next, we’ll create a new React Native project called react-native-test
:
expo init react-native-test cd react-native-test yarn start # you can also use: expo start
This will start a development server. Using expo-cli
to scaffold our React Native project will set up the testing environment for us automatically:
// package.json ... "scripts": { ... "test": "jest --watchAll" }, "jest": { "preset": "jest-expo" } ...
Everything is set for us. All we have to do is run yarn test
in our terminal to run the tests files. Now, we’ll open the project in VS Code, open the integrated terminal, and run yarn test
. We’ll see Jest run the tests files and hang around in watch mode, watching the files so as to rerun the files if entry is made:
PASS components/__tests__/StyledText-test.js (5.018s) ✓ renders correctly (3108ms) › 1 snapshot written. Snapshot Summary › 1 snapshot written from 1 test suite. Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 1 written, 1 total Time: 5.1s Ran all test suites. Watch Usage › Press f to run only failed tests. › Press o to only run tests related to changed files. › Press p to filter by a filename regex pattern. › Press t to filter by a test name regex pattern. › Press q to quit watch mode. › Press Enter to trigger a test run.
Now we can install React Native Testing Library:
yarn add --dev @testing-library/react-native
And that’s it! Now, we’re ready to begin writing our unit tests.
Let’s say we have a Button.tsx
component in the components
folder:
import React from "react"; import { Text, TouchableOpacity } from "react-native"; function Button({ styles, children }) { return ( <TouchableOpacity style={[styles.button]}> <Text>{children}</Text> </TouchableOpacity> ); } export default Button;
We can write a test to determine whether the Button
component renders properly:
// __tests__ import * as React from "react"; import Button from "../Button"; import renderer from "react-test-renderer"; it(`renders correctly`, () => { const tree = renderer.create(<Button>Login</Button>); expect(tree).toMatchSnapshot(); });
Now, let’s write a test for the App
component:
import * as React from "react"; import renderer from "react-test-renderer"; import App from "../App"; it(`renders correctly`, () => { const tree = renderer.create(<App />).toJSON(); expect(tree.children.length).toBe(1); });
The App
renders a DOM with a single child, so this test passes.
We can mock the actual implementation with a dummy version that mimics the real one. Mocking makes tests much faster, especially those that involve internet activity. Jest allows us to add our mock implementations — let’s see how it’s done.
Let’s test a presentational component:
function TodoItem({ todo, editTodoItem, deleteTodoItem }: TodoProps) { return ( <> <View className="todoItem"> <View className="todoItemText"> <Text>{todo.todoText}</Text> </View> <View className="todoItemControls"> <TouchableHighlight className="bg-default" onPress={() => editTodoItem(todo)} > <Text>Edit</Text> </TouchableHighlight> <TouchableHighlight className="bg-danger" onPress={() => deleteTodoItem(todo)} > <Text>Del</Text> </TouchableHighlight> </View> </View> </> ); }
The TodoItem
component renders a todo
. Note that the TodoItem
component expects a todo
object and the functions called when the Del
and Edit
buttons are clicked.
To test this, we will have to mock the function props:
it("delete and edit a todo", () => { const mockEditTodoItemFn = jest.fn(); const mockDeleteTodoItemFn = jest.fn(); const { getByText } = render( <TodoItem todo={{ todoText: "go to church" }} editTodoItem={mockEditTodoItemFn} deleteTodoItem={mockDeleteTodoItemFn} /> ); fireEvent.press(getByText("Edit")); fireEvent.press(getByText("Del")); expect(mockEditTodoItemFn).toBeCalledWith({ 1: { todoText: "go to church" }, }); expect(mockDeleteTodoItemFn).toBeCalledWith({ 1: { todoText: "go to church" }, }); });
We used Jest’s fn()
to create mock functions mockDeleteTodoItemFn
and mockEditTodoItemFn
, then we assigned them to the editTodoItem
and deleteTodoItem
props. The mock functions are called when a user presses the Del
or Edit
buttons. This ensures the buttons will work in production when the real functions are passed.
We can also mock return values:
it("delete and edit a todo", () => { const mockEditTodoItemFn = jest.fn(); mockEditTodoItemFn.mockReturnValue({ edited: true }); const mockDeleteTodoItemFn = jest.fn(); mockDeleteTodoItemFn.mockReturnValue({ deleted: true }); const { getByText } = render( <TodoItem todo={{ todoText: "go to church" }} editTodoItem={mockEditTodoItemFn} deleteTodoItem={mockDeleteTodoItemFn} /> ); fireEvent.press(getByText("Edit")); fireEvent.press(getByText("Del")); expect(mockEditTodoItemFn.mock.results[0].value).toBe({ edited: true, }); expect(mockDeleteTodoItemFn.mock.results[0].value).toBe({ deleted: true, }); });
We called the .mockReturnValue(...)
method on the mock functions with the values we want returned when the functions are called. This helps us test our functions on the type of value it returns.
The property .mock.results
holds the array of results from the mock functions we called. From this, we were able to retrieve the returned values and test them against the expected values.
We can also mock API calls. We’ll use Axios to perform HTTP calls to our APIs. Here’s how we’d mock Axios methods like get
, post
, delete
, and put
:
... import axios from "axios"; export default function Home() { const [todos, setTodos] = useState([]); useEffect(async () => { const result = await axios.get("http://localhost:1337/todos"); setTodos(result?.data); }, []); const addTodo = async (todoText) => { if (todoText && todoText.length > 0) { const result = await axios.post("http://localhost:1337/todos", { todoText: todoText, }); setTodos([...todos, result?.data]); } }; return ( <View> {todos.map((todo,i) => { <Text key={i}>{todo}</Text> })} </View> ); }
We have a component that loads and posts to-dos to a server. To test this component, we will need to mock the HTTP call in Axios:
jest.mock("axios"); it("component can load todos, when mounted.", () => { const resp = { data: [{ todoText: "go to church" }, { todoText: "go to market" }], }; axios.get.mockResolvedValue(resp); const { getByText } = render(<Home />); const todos = [getByText("go to church"), getByText("go to market")]; expect(todos.length).toBe(2); });
Here, we mocked the Axios get
method. When the Home
component loads the to-dos via axios.get
, our mock is called and responds with a mock response. Learn more about mocking here.
We should also test for user interactions in our React Native components — interactions like pressing a button, inputting text, scrolling a list, and so on.
React Native Testing Library is a great resource for testing user interactions in React Native components. It provides methods like getByPlaceholder
, getByText
, and getAllByText
that we can use to get the elements or text from the rendered output as nodes. We can then interact with the rendered nodes as though we were actually doing it in a browser.
Let’s look at our previous example:
it("delete and edit a todo", () => { const mockEditTodoItemFn = jest.fn(); mockEditTodoItemFn.mockReturnValue({ edited: true }); const mockDeleteTodoItemFn = jest.fn(); mockDeleteTodoItemFn.mockReturnValue({ deleted: true }); const { getByText } = render( <TodoItem todo={{ todoText: "go to church" }} editTodoItem={mockEditTodoItemFn} deleteTodoItem={mockDeleteTodoItemFn} /> ); fireEvent.press(getByText("Edit")); fireEvent.press(getByText("Del")); expect(mockEditTodoItemFn.mock.results[0].value).toBe({ edited: true, }); expect(mockDeleteTodoItemFn.mock.results[0].value).toBe({ deleted: true, }); });
The TodoItem
rendered two buttons with the text Del
and Edit
. We used getByText
to access the nodes of these buttons, and fireEvent.press(...)
to press the button, just as we could in an actual browser. This will fire the event handler attached to the button’s onPress
event. This changes the rendered output, and we can test for what happens when buttons are clicked:
function SearchFruit() { const [result, setResult] = useState([]); const [searchItem, setSearchItem] = useState(); const fruits = ["orange", "apple", "guava", "lime", "lemon"]; useEffect(() => { if (searchtiem.length > 0) { setResult(fruits.includes(searchItem)); } }, [searchItem]); return ( <> <TextInput placeholder="Search fruits" onChangeText={(text) => setSearchItem(text)} /> {result.map((fruit, i) => ( <Text key={i}>{fruit}</Text> ))} </> ); }
Above, we have a component that searches for fruits from an array and displays the result if found. We can write tests to ensure that the event handlers are fired when the user interacts with it and that the right results are rendered:
it("search for a fruit when the fruit name is entered/typed", () => { const { getByPlaceholder } = render(<SearchFruit />); fireEvent.changeText(getByPlaceholder("Search fruits"), "guava"); expect(getAllByText("guava")).toHaveLength(1); });
Above, we’re testing that when a user types "guava"
, the correct result is displayed because we have the guava fruit in the fruits
array. The fireEvent.changeText()
method changes the text of an input box, thus firing off the onChange
event handler. getAllByText
gets any occurrence of the text "guava"
in the array. Because we know guava is in the fruits
array, then the text "guava"
should be rendered.
To fire events in React Native components, we use the methods in React Native Testing Library’s fireEvent
API.
press
This fires off the press
event on an element, which calls its attached handler. To fire the press
event, React Native Testing Library exports the fireEvent
object and calls the press()
method. It receives the instance of the element to be pressed as a parameter:
const onPressMock = jest.fn(); const { getByText } = render( <View> <TouchableOpacity onPress={onPressMock}> <Text>Press Me</Text> </TouchableOpacity> </View> ); fireEvent.press(getByText("Press Me")); expect(onPressMock).toHaveBeenCalled();
We set a press
event in the TouchableOpacity
element, with a mocked function handler attached to the event. We used the fireEvent.press()
to “press” the element, so calling the mocked handler. We expect this will call the mocked handler.
changeText
This event inputs text data in an element and fires the changeText
event on the element, which calls the element’s handler:
const onChangeTextMock = jest.fn(); const { getByPlaceholder } = render( <View> <TextInput placeholder="Search fruits" onChangeText={onChangeTextMock} /> </View> ); fireEvent.changeText(getByPlaceholder("Search fruits"), "guava"); expect(onChangeTextMock).toHaveBeenCalled();
We set up a mock handler in the onChangeText
event of the TextInput
box. We fire off the event using fireEvent.changeText(...)
. Now, we expect that the mocked handler is called when fireEvent.changeText(...)
is called on the TextInput
box.
More on events in React Native Testing Library can be found here.
Of course, there is far more to we could say about unit testing in React Native, but this should get you up and running.
We have seen how powerful Jest can be as a testing framework in React-based applications, particularly in React Native. We also saw how useful React Native Testing Library is, with its plethora of query and event methods.
If you have any questions regarding this topic, feel free to leave a comment in the comment section below!
LogRocket is a React Native monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your React Native apps.
LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.
Start proactively monitoring your React Native apps — try LogRocket for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowCompare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.