Chidume Nnamdi I'm a software engineer with 6+ years of experience. I've worked with different stacks ranging from WAMP, to MERN, to MEAN. My language of choice is JavaScript; frameworks are Angular and Nodejs.

Guide to unit testing in React Native

9 min read 2553

Guide Unit Testing React Native

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.

What is unit testing?

Unit testing is the practice of testing small, isolated pieces of code. React Native itself puts it thusly:

Unit tests cover the smallest parts of code, like individual functions or classes.

So unit testing involves testing functions, classes, components — all the tiny bits in our project. 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 wholly 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, those 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; this 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.

Best practices for structuring tests

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. The three conditions are:

We made a custom demo for .
No really. Click here to check it out.

  • When passed 3 hours, it returns 180 minutes
  • When passed 5 hours, it returns 300 minutes
  • When null 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:

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.

Unit testing using Jest and Enzyme

There are many testing frameworks we can use for testing React Native applications, including but not limited to:

  • Mocha
  • Jest
  • Jasmine
  • Nightmare
  • WebDriver

All 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 since React Native isn’t for web applications, neither Jest nor Enzyme has a React Native adapter.

Luckily, there are a couple libraries that can help us out with unit testing for React Native applications:

  1. React Test Renderer
  2. 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:

  1. Interaction, meaning the component responds correctly to the user interaction
  2. Output, meaning the component renders the correct output

Setting up our test environment

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 — we’re ready to begin writing our unit tests.

Testing components

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.

Mocking

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 mock return values as well:

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 mock API calls as well. 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. More on mocking can be found here.

Testing user interactions

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 take 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 use the fireEvent.press(...) method 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 since 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. Since we know guava is in the fruits array, then the text “guava” should be rendered.

Firing events

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

Conclusion

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 awesome React Native Testing Library is, with its plethora of query and event methods.

If you have any questions regarding this topic, or if you feel there’s anything I should add, correct, or remove, feel free to comment, email, or DM me. Thanks!

: Full visibility into your web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Chidume Nnamdi I'm a software engineer with 6+ years of experience. I've worked with different stacks ranging from WAMP, to MERN, to MEAN. My language of choice is JavaScript; frameworks are Angular and Nodejs.

Leave a Reply