DEV Community

Cover image for How to mock API calls in React Native
Kimmo Sääskilahti for Meeshkan

Posted on • Originally published at meeshkan.com

How to mock API calls in React Native

Testing networking logic in React Native apps can be hard. You don't want to use the production API to run tests, so you need to mock network calls. Mocking also lets you test both the happy case where API works as expected as well as the case where the API fails.

There are different ways to mock network calls. You could use dependency injection to inject "fetching service" into the components. In tests, you would replace the real service with a mock. Or you could use Context to wrap components in a "fetching service" context. Both of these solutions can work, but there should be a simpler way.

In this post, we are going to build a basic React Native application tested in end-to-end fashion. We use Unmock to serve mock data to the app. Unmock is an HTTP testing library using node-mitm behind the scenes to intercept HTTP traffic. At interception, it generates random data mocking the API.

We'll run our tests in Node.js with Jest. We use React Native Testing Library to render the component and trigger React hooks. You can find the repository for this project here. Repository also includes instructions for running the app.

Tour of the sample application

The sample application shows a random cat fact fetched from the Cat Facts API. User can refresh the fact by pressing the button. The app in all its glory looks like this, running here in Android virtual device:

Screenshot of the sample application

Code for the app contains a single component defined in App.tsx. At high-level, we define the App component like this:

const App = () => {
  /* React hooks */
  const [shownFact, setFact] = useState('');
  const [err, setError] = useState(null);
  const [loading, setLoading] = useState(false);

  /* Refresh cat fact, see below */
  const refreshFact = async () => {
    /* */
  };

  /* Initial data fetching */
  useEffect(() => {
    refreshFact();
  }, []);

  return (
    /* JSX, see below */
  );
};
Enter fullscreen mode Exit fullscreen mode

We use useState from React hooks for managing the state of shownFact, err, and loading. These variables contain the cat fact displayed to the user, possible fetch error, and the loading state.

The refreshFact function refreshes the cat fact shown to the user:

const refreshFact = async () => {
  try {
    setLoading(true);
    const fact = await fetchFact();
    setFact(fact);
    setError(null);
  } catch (err) {
    setError(err);
  } finally {
    setLoading(false);
  }
};
Enter fullscreen mode Exit fullscreen mode

This function sets the component state and uses fetchFact function for the network call. The fetchFact function uses Fetch API provided by React Native:

const CAT_FACT_URL =
  'https://cat-fact.herokuapp.com/facts/random?animal_type=cat&amount=1';

const fetchFact = async () => {
  const fetchResult = await fetch(CAT_FACT_URL);
  if (!fetchResult.ok) {
    throw Error(`Failed fetching cat fact with code: ${fetchResult.status}`);
  }
  const body = await fetchResult.json();
  const fact = body.text;
  return fact;
};
Enter fullscreen mode Exit fullscreen mode

We parse the body by first parsing a JSON and extract the cat fact from the text property as documented here.

Application component renders content based on the values of loading and err:

{
  loading ? (
    <Text style={styles.loading} testID="loading">
      Loading...
    </Text>
  ) : err ? (
    <Text style={{...styles.fact, ...styles.error}} testID="error">
      Something went horribly wrong, please try again!
    </Text>
  ) : (
    <Text style={styles.fact} testID="fact">
      {shownFact}
    </Text>
  );
}
Enter fullscreen mode Exit fullscreen mode

If the state of loading is true, we show the text "Loading...". If the state of err contains an error, user will see an apology. Otherwise, app shows the cat fact.

Note that we also give the components testID properties to simplify testing.

Writing tests

Prerequisites

File App.test.tsx contains the tests. The first step in the tests is to fill in fetch (not available in Node.js) with node-fetch:

// @ts-ignore
global.fetch = require('node-fetch');
Enter fullscreen mode Exit fullscreen mode

In the beforeAll block, we switch on Unmock with unmock.on(). Then we add rules for intercepting all outgoing traffic for the Cat Facts API URL:

beforeAll(() => {
  unmock.on();
  unmock
    .nock('https://cat-fact.herokuapp.com', 'catFactApi')
    .get('/facts/random?animal_type=cat&amount=1')
    .reply(200, {text: u.string('lorem.sentence')})
    .reply(500, 'Internal server error');
});
Enter fullscreen mode Exit fullscreen mode

In unmock.nock call, we also a give the name catFactApi for the created fake service. Later in tests, we use the catFactApi name to change the behaviour of the service.

In the behaviour for status code 200, we specify that the API should return a JSON body with text property. The syntax u.string('lorem.sentence') means that the value should be a fake sentence. See faker.js for other kinds of fake values you can use. Notice how we don't need to hardcode "foo" or "bar" in our tests!

Before each test, we reset the state of unmock so that the tests remain decoupled:

beforeEach(() => {
  unmock.reset();
});
Enter fullscreen mode Exit fullscreen mode

Test for success

The first test ensures that when the API returns a cat fact, the app contains the correct element:

it('renders the fact block when API succeeds', async () => {
  const api = unmock.services['catFactApi'];
  api.state(transform.withCodes(200));
  const renderApi: RenderAPI = render(<App />);

  await waitForElement(() => {
    return renderApi.getByTestId('fact');
  });
});
Enter fullscreen mode Exit fullscreen mode

Here we first set the API to always return 200, simulating success. We then use render from library to render the component and run all hooks. We use waitForElement to wait for the element with testID="fact" to show up.

Second test for success ensures that when user clicks the button, the app fetches a new fact from the API. We simulate button press with the fireEvent from react-native-testing-library:

it('renders new fact after clicking the button', async () => {
  const api = unmock.services['catFactApi'];
  api.state(transform.withCodes(200));

  const renderApi: RenderAPI = render(<App />);

  fireEvent.press(renderApi.getByText('Get me a new one'));

  await waitForElement(() => {
    const secondCall = api.spy.secondCall;
    const secondFact = secondCall.returnValue.bodyAsJson;
    return renderApi.getByText(secondFact.text);
  });
});
Enter fullscreen mode Exit fullscreen mode

Here we again use waitForElement like above. This time we wait for an element containing the same text as the random fact returned from the API. Because the API returns a random sentence, we need to find its value. Unmock services keep track of mocked calls in the spy property. This property is a SinonJS spy. The spy exposes its second call via the secondCall property. The return value of that call is in returnValue. See the chapter on expectations in Unmock documentation for more information.

Test for failure

Test for failure proceeds as the test for success. we change the API to return status code 500, render the app, and wait for the element with testID="error" to show up.

it('renders error when the API fails', async () => {
  const api = unmock.services['catFactApi'];
  api.state(transform.withCodes(500));

  const renderApi: RenderAPI = render(<App />);

  await waitForElement(() => {
    return renderApi.getByTestId('error');
  });
});
Enter fullscreen mode Exit fullscreen mode

Conclusion

That's it! Using Unmock, Jest and React Native Testing Library, we wrote comprehensive integration tests for our component. The tests made sure that the app triggers data fetching via React hooks. We also ensured that the app displays the returned cat fact without hardcoding "foo" or "bar". We also tested the case when the API call fails. We did not need to inject extra dependencies into our component or use contexts to mock the API.

Note that unmock currently only supports Node.js environment. If you would like to see Unmock populate your React Native app with fake data, create an issue in unmock-js repository.

Thanks a lot for reading, as always we appreciate any feedback and comments!

Top comments (0)