Integration Testing for React Native Apps

Gleb Bahmutov

Sr Director of Engineering

React

Finland 2021

You want to write a mobile app... no problem

  • learn React
  • npx react-native init MyProject
  • Profit!

Why we decided to rewrite our iOS & Android apps from scratch — in React Native. Mercari US

import React from 'react';
import {Text, View} from 'react-native';
import {Header} from './Header';
import {heading} from './Typography';

const WelcomeScreen = () => (
  <View>
    <Header title="Welcome to React Native"/>
    <Text style={heading}>Step One</Text>
    <Text>
      Edit App.js to change this screen and turn it
      into your app.
 ...
import React from 'react';
import {Text, View} from 'react-native';
import {Header} from './Header';
import {heading} from './Typography';

const WelcomeScreen = () => (
  <View>
    <Header title="Welcome to React Native"/>
    <Text style={heading}>Step One</Text>
    <Text>
      Edit App.js to change this screen and turn it
      into your app.
 ...

Are you sure it is working?

  • ESLint + Flow / Typescript
  • Jest for unit and component tests
    • jsDOM, synthetic events
  • Detox / Appium for built native application
  • Render React RN app in the browser
    • Use Cypress.io test runner

A simple RN app

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

export default function App() {
  const [isLoading, setLoading] = useState(true);
  const [data, setData] = useState([]);

  useEffect(() => {
    fetch('https://jsonplaceholder.cypress.io/users')
      .then((response) => response.json())
      .then((json) => setData(json))
      .catch((error) => console.error(error))
      .finally(() => setLoading(false));
  }, []);

  return (
    <View style={{ flex: 1, padding: 24 }}>
      {isLoading ? <ActivityIndicator testID="loading"/> : (
        <FlatList
          accessibilityLabel="users"
          data={data}
          keyExtractor={({ id }, index) => String(id)}
          renderItem={({ item }) => (
            <Text testID="user" accessibilityLabel="user">
              {item.name}, {item.email}</Text>
          )}
        />
      )}
    </View>
  );
};

App.js

Test the Web Version!

Assume the Expo translated the RN code into the same functionality for the Web, iOS, and Android

{
  "baseUrl": "http://localhost:19006",
  "viewportWidth": 500,
  "viewportHeight": 700
}

cypress.json

it('loads a list of users', () => {
  cy.visit('/')
  cy.get('[data-testid=user]')
    .should('have.length.gt', 3)
})

spec.js

Spy on the network call

it('spy on the network load', () => {
  cy.intercept('/users').as('users')
  cy.visit('/')
  cy.wait('@users').its('response.body')
    .should('be.an', 'Array')
    .and('have.length.gt', 5)
    .then(users => {
      cy.get('[data-testid=user]')
        .should('have.length', users.length)
    })
})

spec.js

Spy on the network call

it('spy on the network load', () => {
  cy.intercept('/users').as('users')
  cy.visit('/')
  cy.wait('@users').its('response.body')
    .should('be.an', 'Array')
    .and('have.length.gt', 5)
    .then(users => {
      cy.get('[data-testid=user]')
        .should('have.length', users.length)
    })
})

spec.js

Loading indicator

it('shows the loading indicator', () => {
  // slow down the response by 1 second
  // https://on.cypress.io/intercept
  cy.intercept('/users', (req) => {
    return req.continue(res => res.setDelay(1000))
  }).as('users')
  cy.visit('/')
  // the loading indicator should be visible at first
  cy.get('[data-testid=loading]').should('be.visible')
  // the disappear
  cy.get('[data-testid=loading]').should('not.exist')
  cy.wait('@users')
})

spec.js

import { ActivityIndicator } from 'react-native';
{isLoading ? <ActivityIndicator testID="loading"/> ...

App.js

Loading indicator

it('shows the loading indicator', () => {
  // slow down the response by 1 second
  // https://on.cypress.io/intercept
  cy.intercept('/users', (req) => {
    return req.continue(res => res.setDelay(1000))
  }).as('users')
  cy.visit('/')
  // the loading indicator should be visible at first
  cy.get('[data-testid=loading]').should('be.visible')
  // the disappear
  cy.get('[data-testid=loading]').should('not.exist')
  cy.wait('@users')
})

spec.js

import { ActivityIndicator } from 'react-native';
{isLoading ? <ActivityIndicator testID="loading"/> ...

App.js

Stub network

it('shows mock data', () => {
  cy.intercept('/users', { 
    fixture: 'users.json' 
  })
  cy.visit('/')
  cy.get('[data-testid=user]')
    .should('have.length', 3)
})

spec.js

Network errors

cy.intercept('/users', (req) => {
  return Cypress.Promise.delay(1000)
    .then(() => req.reply({ forceNetworkError: true }))
})
// observe the application's behavior
// in our case, the app simply logs the error
cy.visit('/', {
  onBeforeLoad(win) {
    cy.spy(win.console, 'error').as('logError')
  },
})
cy.get('[data-testid=loading]').should('be.visible')
// confirm the loading indicator goes away
cy.get('[data-testid=loading]').should('not.exist')
cy.get('@logError').should('have.been.called')

spec.js

useEffect(() => {
  fetch('https://jsonplaceholder.cypress.io/users')
    .then((response) => response.json())
    .then((json) => setData(json))
    .catch((error) => console.error(error))
    .finally(() => setLoading(false));
}, []);

App.js

Network errors

cy.intercept('/users', (req) => {
  return Cypress.Promise.delay(1000)
    .then(() => req.reply({ forceNetworkError: true }))
})
// observe the application's behavior
// in our case, the app simply logs the error
cy.visit('/', {
  onBeforeLoad(win) {
    cy.spy(win.console, 'error').as('logError')
  },
})
cy.get('[data-testid=loading]').should('be.visible')
// confirm the loading indicator goes away
cy.get('[data-testid=loading]').should('not.exist')
cy.get('@logError').should('have.been.called')

spec.js

useEffect(() => {
  fetch('https://jsonplaceholder.cypress.io/users')
    .then((response) => response.json())
    .then((json) => setData(json))
    .catch((error) => console.error(error))
    .finally(() => setLoading(false));
}, []);

App.js

Is the app accessible?

RN supports Aria attributes and test IDs

<View style={{ flex: 1, padding: 24 }}>
  {isLoading ? (
    <ActivityIndicator
      testID="loading"
      accessibilityLabel="App is loading users"
    />
  ) : (
    <FlatList
      accessibilityLabel="users"
      data={data}
      keyExtractor={({ id }, index) => String(id)}
      renderItem={({ item }) => (
        <Text testID="user" accessibilityLabel="user">
          {item.name}, {item.email}
        </Text>
      )}
    />
  )}
</View>

App.js

Is the app accessible?

it('is accessible', () => {
  cy.intercept('/users', {
    fixture: 'users.json',
    delay: 2000,
  })
  cy.visit('/')
  cy.get('[aria-label="App is loading users"]').should('be.visible')
  cy.get('[aria-label="users"]')
    .should('be.visible')
    .get('[aria-label=user]')
    .should('have.length', 3)
})

spec.js

Is the app accessible?

it('is accessible', () => {
  cy.intercept('/users', {
    fixture: 'users.json',
    delay: 2000,
  })
  cy.visit('/')
  cy.get('[aria-label="App is loading users"]').should('be.visible')
  cy.get('[aria-label="users"]')
    .should('be.visible')
    .get('[aria-label=user]')
    .should('have.length', 3)
})

spec.js

Is the app accessible?

$ npm install --save-dev cypress-axe axe-core
+ cypress-axe@0.12.2
+ axe-core@4.2.2

Full a11y testing

spec.js

cy.visit('/')
  ...
// a11y check
cy.injectAxe();
cy.checkA11y();
$ npm install --save-dev cypress-axe axe-core
+ cypress-axe@0.12.2
+ axe-core@4.2.2

Full a11y testing

spec.js

cy.visit('/')
  ...
// a11y check
cy.injectAxe();
cy.checkA11y();

Did we test everything?

Yes, we did.

module.exports = function(api) {
  api.cache(true);
  return {
    presets: ['babel-preset-expo'],
    plugins: ['istanbul']
  };
};

babel.config.js

+ @cypress/code-coverage

Does the app look good?

the same

Use any Cypress visual plugin

https://on.cypress.io/visual-testing

spec.js

cy.visit('/')
  ...
// visual snapshot
cy.percySnapshot('two items')

Does the app look good?

the same

Use any Cypress visual plugin

https://on.cypress.io/visual-testing

spec.js

cy.visit('/')
  ...
// visual snapshot
cy.percySnapshot('two items')

Does it work?

Does Expo guarantee Web = iOS = Android?

  • I trust Expo that is used by 1000s of projects more than my own code
  • Testing Web version in Expo tests my code, my logic, my data, my backend
  • For native components that Expo cannot show in the browser = mock them

Thank You 👏

React

Finland 2021