Gleb Bahmutov PRO
JavaScript ninja, image processing expert, software quality fanatic
📹 Video at https://www.youtube.com/watch?v=PIxaFbMBez0
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.
...
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
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
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
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
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
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
it('shows mock data', () => {
cy.intercept('/users', {
fixture: 'users.json'
})
cy.visit('/')
cy.get('[data-testid=user]')
.should('have.length', 3)
})
spec.js
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
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
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
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
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
$ npm install --save-dev cypress-axe axe-core
+ cypress-axe@0.12.2
+ axe-core@4.2.2
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
spec.js
cy.visit('/')
...
// a11y check
cy.injectAxe();
cy.checkA11y();
module.exports = function(api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
plugins: ['istanbul']
};
};
babel.config.js
+ @cypress/code-coverage
Use any Cypress visual plugin
spec.js
cy.visit('/')
...
// visual snapshot
cy.percySnapshot('two items')
Use any Cypress visual plugin
spec.js
cy.visit('/')
...
// visual snapshot
cy.percySnapshot('two items')
By Gleb Bahmutov
My unpopular opinion is that testing is ... important. How do you test your React Native apps? In this presentation, I will show how to run full integrations tests using Cypress while the RN app is running in the browser. This method can cover most of the application's code and be effective at finding logical errors and mistakes when calling the server APIs. Presented at ReactFinland 2021. Find the video at https://www.youtube.com/watch?v=PIxaFbMBez0
JavaScript ninja, image processing expert, software quality fanatic