Testing React Native With Jest, react-test-renderer And react-native-testing-library

Published on 5 years ago

React Native is a great framework for you to write once and build mobile app with Javascript and React to both Android and iOS platform. Everything is pretty easy until your superior telling you, "We need to start practice TDD approach because there are too many bugs surfaced in production to capture the error early". Sounds familiar? 

Okay, today we will look into how to test React Native components using Jest and react-test-renderer as well as react-native-testing-library. There are few common scenarios that you will need to test for a component and we will cover them in this post.

  • Snapshot testing
  • Event triggering
  • Async API call
  • Timer event

In this post, all the components will be using React Hooks

Testing component with Snapshot testing

Snapshot testing allow us to easily verify if the component​ UI is work as expected and not changing unexpectedly. Jest will capture a snapshot of the component when the first time you run the test and verify the result against the snapshot for the subsequence test run. If the result is not same as snapshot, it will fail the tests.

Now let's try it with HelloWorldComponent below.

import React from 'react';
import { Text } from 'react-native';

const HelloWorldComponent = () => {
  return (
    Hello World
  );
};

export default HelloWorldComponent;

And here is the test looks like

import React from 'react';
import renderer from 'react-test-renderer';

import HelloWorldComponent from '../HelloWorldComponent';

describe('HelloWorldComponent', () => {
  test('should display Hello World', () => {
    // Render a React component
    const component = renderer.create();

    // Expect the result
    expect(component.toJSON()).toMatchSnapshot();
  });
});

Your snapshot taken will be look like this.

exports[`HelloWorldComponent should display Hello World 1`] = `

  Hello World

`;

It is very hard to do snapshot testing right as when your components is complex, you will have a hard time to check on the snapshot (With your eyes i mean). Here is some advice from me: Iterate the component piece by piece and commit your changes to source control so that you can verify the changes for each iteration is correct.

Testing Event Trigger

Another scenario that we commonly need to test is event triggering, such as button click. Let's add a Button in the HelloWorldComponent.

import React, { useState } from 'react';
import { Button, View, Text } from 'react-native';

const HelloWorldComponent = () => {
  const [text, setText] = useState('Hello World');

  const onButtonPress = () => {
    setText('I have pressed the button.');
  };

  return (
    
      

We added a Button that will change the text to "I have pressed the button" after it is pressed. You will realize that we have added "testID" props for the Button, this props will be used to look up the Button and trigger the onPress() event during the test. react-test-render provide few methods for you to look up the descendent component, you are free to use any of those look up methods.

​Now, let's test the onPress event.

import React from 'react';
import renderer, { act } from 'react-test-renderer';

import HelloWorldComponent from '../HelloWorldComponent';

describe('HelloWorldComponent', () => {
  test('should change text after press button', () => {
    // Render a React component
    const component = renderer.create();

    // Find the button that has props.testID === 'button'
    const button = component.root.findByProps({ testID: 'button' });

    // All codes that causes state updates should wrap in act(...)
    act(() => {
      // Call the onPress props of the button
      button.props.onPress();
    });

    // Expect the result
    expect(component.toJSON()).toMatchSnapshot();
  });
});

You should see the text is displaying as "I have pressed the button.". In this test, we use "act" to wrap the button press event trigger because all events that causes the state updates should wrap in "act", otherwise, the changes will not reflect to the component.

Testing Asynchronous Event

Another very common scenario is that we will retrieve some data from server and display it when we enter a page. Let's change our HelloWorldComponent to become that scenario now.

import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { Text } from 'react-native';

const HelloWorldComponent = () => {
  const [text, setText] = useState('Hello World');

  useEffect(() => {
    const getTextFromServer = async () => {
      // Get response from server
      const response = await axios.get('https://example.com');

      // Set text
      setText(response.data);
    };

    getTextFromServer();
  }, []);

  return (
    {text}
  );
};

export default HelloWorldComponent;

We have left the dependencies of "useEffect" as empty so that it's only trigger once, which similar to "componentDidMount". Now let's try to write the test.

import React from 'react';
import renderer from 'react-test-renderer';
import axios from 'axios';

import HelloWorldComponent from '../HelloWorldComponent';

describe('HelloWorldComponent', () => {
  test('should display text from server', async () => {
    // Mock response from axios
    jest.spyOn(axios, 'get').mockReturnValue({ data: 'This is server response' });

    // Render a React component
    const component = renderer.create();

    // Expect the result
    expect(component.toJSON()).toMatchSnapshot();
  });
});

It's pretty simple, isn't it? We just need to mock the response for "axios" and the test should be complete. Let's run the test and wait for green light! ...........Erm......let us check on the snapshot....oh no...the text is still remaining as "Hello World". What is wrong here? Don't worry, this is working as expected...If you are familiar with "useEffect", you will know it will run after the component layout and painted. You can read more for the timing of effect docs. There are thorough discussion happening in this issue about how to solve this problem but today I wanted to solve this problem using utils called react-native-testing-library which highly inspired by react-testing-library. This library provided some useful function to re-render component or clear all async tasks. Let's make some changes to our test.

import React from 'react';
import { render, flushMicrotasksQueue } from 'react-native-testing-library';
import axios from 'axios';

import HelloWorldComponent from '../HelloWorldComponent';

describe('HelloWorldComponent', () => {
  test('should display text from server', async () => {
    // Mock response from axios
    jest.spyOn(axios, 'get').mockReturnValue({ data: 'This is server response' });

    // Render a React component
    const component = render();

    // Flush all tasks queued
    await flushMicrotasksQueue();

    // Expect the result
    expect(component.toJSON()).toMatchSnapshot();
  });
});

There are 2 things we changed here:
1. We are no longer directly using react-test-renderer to render the component, instead we use { render } from react-native-testing-library.
2. We added "await flushMicrotasksQueue()" to flush all the queued tasks in JS.
Now, we can re-run the test and we should see the display text as "This is server response".

Testing Timer Event

Last thing we need to look into today is testing timer event, there are also occasion that we will need to setTimeout to trigger some changes or setInterval to repeatedly trigger some polling. Let's add some time out event in our HelloWorldComponent.

import React, { useState, useEffect } from 'react';
import { Text } from 'react-native';

const HelloWorldComponent = () => {
  const [text, setText] = useState('Hello World');

  useEffect(() => {
    const timer = setTimeout(() => {
      setText('Timer is done!');
    }, 3000);

    return () => {
      clearTimeout(timer);
    };
  }, []);

  return (
    {text}
  );
};

export default HelloWorldComponent;

Now, HelloWorldComponent will change the text to "Timer is done!" after 3 seconds, but how can we test it after 3 seconds? Are we going to wait 3 seconds in the test? Of course no! We can use "jest.useFakeTimers()" to mock the timer and run them immediately. Here is how it's looks for the test:

import React from 'react';
import { render } from 'react-native-testing-library';

import HelloWorldComponent from '../HelloWorldComponent';

describe('HelloWorldComponent', () => {
  test('should change text after 3 seconds', async () => {
    // Fake timer
    jest.useFakeTimers();

    // Render a React component
    const component = render();

    // Tell jest to run remaining timer and not create any new timer
    jest.runOnlyPendingTimers();

    // Expect the result
    expect(component.toJSON()).toMatchSnapshot();
  });
});

All we need to do is to fake the timer and tell jest to run the timer, then we got the result we wanted!

For this post, we have looked in to how to test for component using snapshot, event triggering, async event as well as timer event using Jest, react-test-renderer or react-native-testing-library. If you would like to use react-native-testing-library to test all the scenarios, you are free to do so too.

Thank you for reading!

Copyright © 2024 Tek Min Ewe