The ability to write cross platform apps with React Native has brought iOS and Android developers together, committing to the same repository and allowing us to share the majority of our code across platforms. While sharing code has halved the amount of work, it also creates the problem that if something breaks, its broken on both platforms. This means writing and maintaining a comprehensive suite of tests is vitally important in making sure the apps we build keep working the way we expect.
There are a number of different ways we can test our React Native apps. Here at 3 Sided Cube we’ve found some techniques to be more effective for our particular use cases. In this blog post I’ll just be covering how to setup and write basic Unit tests, and how to mock network requests with Jest. More specific testing topics such as testing React components and acceptance testing will be explained in future posts. Unit tests have been extremely useful for stopping regressions, and makes sure that the code we build is created in a modular, reusable way.
Because React Native is written in Javascript, we can take advantage of the large amount of free and open source tooling available for writing and running tests.
Facebook has its own testing framework called Jest, which helpfully automatically mocks any dependencies your project relies on, while still giving ultimate control over each test. Jest also provides everything else you would expect from a testing framework, including the ability to test asynchronous functions and a wide variety of assertions offered by Jasmine.
Jest is easy to get setup within your React Native project, just install jest with npm i jest -g
and then configure its settings in your package.json
as shown here.
As a quick example, I’ll show you two of the tests for the bindString method which Simon discussed in his blog post on content as an example.
The bindString method takes in a string to be bound as the first argument, and an object with state to bind to the string as the second. It should return a new string with the values bound.
/* unmock the vieBinder module so we don't get a jest's automatic mock */ jest.unmock("../viewBinder"); describe("bindString", () => { it("returns the given string if no \"{}\" are found", () => { const result = viewBinder.bindString("Hello world", {}); expect(result).toBe("Hello world"); }); it("binds variable inside \"{}\" to the state and returns the full string", () => { const result = viewBinder.bindString("Hello {name}", {name: "Joel"}); expect(result).toBe("Hello Joel"); }); });
Jest looks for test files located under folders named __tests__
, which can either sit in the project root level directory, or inside the same directory as the file you’re testing. I’ve found the latter approach easier to manage when writing tests as you can quickly find and switch between the project and test files.
To run the tests you simply enter jest
from the terminal, you can optionally pass in a directory to only run particular tests like jest ./someDirectory/
.
These two tests ensure the viewBinder works as we expect, binding the value inside of the curly braces to the respective value inside the object.
Jasmine also provides many other useful assertions such as toEqual()
which deeply compares rather than just performing a standard ===
letting you assert javascript objects and arrays as well.
So far I’ve only shown a simple example, more complicated functionality that requires external dependencies such as networking requires a bit more effort. To make sure your unit tests run fast, and are not effected by varying network conditions, you should stub out any requests to the server. This means rather than actually performing a network request, we can instead create and return a fake response, which will let us test our code without waiting for a response from an API.
React Native lets you perform network requests with the handy fetch
function, fetch is provided as a global function, rather than a module that needs to be required. Initially this threw me as I wasn’t sure how I would be able to mock it, thankfully jest will just let you mock any function.
const mockResponse = {someData: 123} fetch = jest.fn((url, options) => new Promise((resolve, reject) => { resolve( { status: 201, json: () => (mockResponse) }) }));
Here we redefine fetch
as a new function, which returns a Promise, that immediately gets resolved with a response object that we can then test with.
However if we want to test multiple end points, constantly redefining fetch
quickly becomes annoying and error prone, instead we can use a handy mocking module fetch-mock that lets us quickly and easily stub the fetch function with the fake response we want.
Install with npm i fetch-mock --save-dev
Now we can write our network test:
/** import fetch-mock */ fetchMock = require("fetch-mock"); /** import some fixture json data to use as a response */ const mockResponseEvents = require("./mockEventDataResponse.json"); describe("fetchEvents", () => { /** Before each is run before the start of each test*/ beforeEach(() => { /** We call restore() on fetchMock to reset any old stubs we defined in other tests*/ fetchMock.restore(); }); const url = CharityKitConstants.API_URL + "/content/events"; /** We call `pit` rather than `it` as the test is asynchronous */ pit("resolves fetched data correctly", () => { /** Setup the stub for this url with the response object */ fetchMock.mock(url, mockResponseEvents); /* Call our network request function and return its promise for the test */ return api.fetchEvents(identity).then(value => { /*Perform our assertions as normal */ expect(value.constructor.name).toEqual("PaginatedData"); expect(value.data.length).toBe(2); expect(value.data[0].name).toBe("Playing Mario for 48 hours for charity"); }); })
We can test that our code correctly handles the result of the network request i.e a particular model is created with values we expect.
We can also test our app handles errors by changing the .mock()
method call to a status code of our choice.
pit("rejects with 400", () => { fetchMock.mock(url, 400); return api.fetchEvents({}) .then(value => { }) .catch(err => { expect(err).toEqual(new Error("Could not retrieve events. Response status 400")); }); });
Jest also comes with code coverage support through Instanbul.js, and is as simple as entering jest --coverage
in the terminal. This will print the results out, and create a webpage with the results under a /coverage directory.
Overall Unit tests with Jest, mean we can quickly find out when we’ve broken existing functionality, and allows us be more confident when making large changes to our codebase.
Published on May 18, 2016, last updated on March 15, 2023