React Native: Testing with Jest

It’s a painful road getting an existing project set up with tests. An existing React Native app of ours, out in the wild, is getting retrofitted with unit tests.

The great part is, I know what to test. I know what type of bugs have been introduced in the past and what we’re likely to break again (exactly what the tests are for…)

“it renders without crashing”.

The out of the box test ensures your app renders without crashing. It’s also where you get to find out about any of your libraries that are incompatible with jest! For me, this was ‘sentry-expo’.

Simply updating to the most recent expo along the way had me on my way again.

 

import React from 'react';
import App from './App';

import renderer from 'react-test-renderer';

it('renders without crashing', () => {
  const rendered = renderer.create().toJSON();
  expect(rendered).toBeTruthy();
  expect(rendered).toMatchSnapshot();
});

Note we’ve added an extra test (shown in bold) to take a DOM snapshot and check it against previous snapshots.

If the initial DOM tree changes in any way to the stored snapshot, the test will fail. This is a great way to capture unintended side-effects and loading issues. Once you’ve investigated the change (or pre-empted it..) you can tell the testrunner to update the snapshot for the future.

We keep “npm run test” alive in the background to see the tests constantly run as I make code changes, add new tests, and modify existing tests.

Testing a real component

Our retail price component accepts either a product or a price prop, and decides a price to show the user based on this. We also show the currency code decided by the app owner.

The component is in “components/Price.js”, so if we create a new file called “components/Price.test.js”, jest will find it and add it to the test suite automatically.

 <Price price='9.99'/>

or

 <Price product={{price: 101}}/>

Our first test creates a component with price of 9.99 and checks the state has been set correctly.

 

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

// the component in test
import Price from './Price';
// the global business settings (like currency symbol)
import Business from "../skin/business";


it('can-show-prices', () => {
 let pricetag = renderer.create(<Price price='9.99'/>);
 
// check the Text component (with class 'actualPrice') contains $9.99
 let pricetagroot = pricetag.root;
 expect(pricetagroot.findByProps({className: "actualPrice"}).props.children).toEqual(["$", "9.99"]);
 
// check the snapshot matches, and make sure the price makes its way to state properly
 let pricetagTree = pricetag.toTree()
 expect(pricetagTree).toMatchSnapshot();
 expect(pricetagTree.props.price).toBe('9.99');
 expect(pricetagTree.instance.state.price).toBe('9.99');

// Let's change the currency symbol for the app, and check it propagates to a component.
 Business.business.currency_symbol = "@";
 
// Pass in an object instead of a value.
 pricetag = renderer.create(<Price product={{price: 101}}/>);
 
// Make sure the new currency symbol and the price finds its way to the display (also, number formatting!)
 pricetagroot = pricetag.root;
 expect(pricetagroot.findByProps({className: "actualPrice"}).props.children).toEqual(["@", "101.00"]);
 
// Check the snapshot and that the price propagated correctly.
 pricetagTree = pricetag.toTree();
 expect(pricetagTree).toMatchSnapshot();
 expect(pricetagTree.instance.state.product.price).toBe(101);

});

Testing an external HTTP request

Testing an external HTTP request is strictly an integration test, not a unit test. It’s generally frowned upon to include ajax requests in your test suite, because you need to test your current code is working (the rest of the pipeline is a different problem, strictly).

However, an integration test that includes some end to end components holds a huge amount of value – knowing an API endpoint is still up and responding, authenticating properly, and that little used areas of your app are running as they should.

In our React Native context, calling an endpoint using our api.js file ensures that the current API adapter, as is in the codebase, can authenticate a user or initiate a payment.

To get ajax working correctly in jest, we need to require xhr2 first. Our underlying API adapter uses fetch(), which isn’t defined in jest.

const XMLHttpRequest = require('xhr2');

global.XMLHttpRequest = XMLHttpRequest;

Next up, we call the endpoint and investigate the response. This test calls the endpoint with invalid data, and ensures that the request fails (just as important as making sure a valid request passes!)

it("Should throw errors on incomplete request.", (done) => {
    return Woo.customers.authenticate("[email protected]").then( data => {
        expect(data).toEqual(expect.objectContaining({result:false,user:null,errors:expect.anything()}));
        expect(data.errors).toEqual(expect.objectContaining({invalid_username: expect.anything()}));
        done();
    }).catch(data => {
        console.log(data);
        done.fail();
    });
});

Woo.customers.authenticate is the API request exactly as we make it in the React Native app, using the same included JS file.

We interrogate the JSON response – the first expect ensures we receive an object with result set to false, user set to null, and an errors object (with anything in it).

The second expect ensures that the errors object has a property “invalid_username“.

We call ‘done()’ to let the test container know we’re finished (since we’re buried in a promise) – done() is a callback passed in to it().

More Jest & React Native testing snippets

It can be helpful to see some real world examples of tests – especially in jest I find the documentation a bit light on ‘real world’ tests. Here’s some Jest snippets from a React Native project:

Make sure string matches in JSON response.

expect(data.email).toEqual("[email protected]");

Make sure the Header component rendered with these props matches the last time it passed successfully.

let header = renderer.create(<Header title='test 234' showHamburger={true} isSearch={true} showCart={true} />);
expect(header).toMatchSnapshot();

Make sure the data object returned has a ‘liveMode’ property (which can be anything)

expect(data).toEqual(expect.objectContaining({liveMode: expect.anything()}));

Render a component. Then pass new props to component, and check the props rendered correctly as per last time:

let headerRenderer = renderer.create(<Header showHamburger={true} isSearch={false} showBackButton={true} showCart={true}/>);
headerRenderer.update(<Header title={"RHYS"} showHamburger={true} isSearch={false} showBackButton={true} showCart={true}/>);


let headerJSON = headerRenderer.toJSON();
expect(headerJSON).toMatchSnapshot();

Good luck!

 

Published