Quick Guide to React Native Testing

Because you’d rather be writing your app than tests

Photo by Kieran Wood on Unsplash

Why Write Tests?

Unit, Integration or End-to-end?

What Are Good Tests?

Example App

npx react-native run-ios
node server/app.js
npm test

Testing Components

// Todo.test.jsconst useDispatchMock = jest.fn();
jest.spyOn(reactRedux, 'useDispatch').mockReturnValue(useDispatchMock);
describe('Todo', () => {
it('Handles user actions', () => {
const renderer = TestRenderer.create(
<Todo id={ 123 } done={ false } />
);
const root = renderer.root;
const checkbox = root.findByType(Checkbox);
checkbox.props.onChange(true);
expect(useDispatchMock).toHaveBeenCalledWith(Actions.toggleTodo(123, true));
});
});
it('Handles done state', () => {
const renderer = TestRenderer.create(
<Todo text={ 'Todo' } done={ true } />
);
const root = renderer.root;
const checkbox = root.findByType(Checkbox);
const text = root.findByProps({ children: 'Todo' });
const doneStyle = {
textDecorationLine: 'line-through',
color: Colors.todoDone,
};
expect(checkbox.props.checked).toBe(true);
expect(text.props.style).toContainEqual(doneStyle);
});

Testing Actions

// actions.test.jsit('Creates CREATE_TODO action', () => {
const action = { type: Actions.CREATE_TODO, text: 'Todo' };
expect(Actions.createTodo('Todo')).toStrictEqual(action);
});
import thunk from 'redux-thunk';
import configureMockStore from 'redux-mock-store';
const mockStore = configureMockStore([ thunk ]);describe('Async actions', () => {
it('Handles successful fetch', () => {
const store = mockStore();
const todos = [{ text: 'Text', id: 123, done: false }];
// Mock async API method success
const fetchTodosMock = jest.spyOn(Api, 'fetchTodos').mockResolvedValue(todos);
const expectedActions = [
Actions.fetchTodos(),
Actions.fetchTodosSuccess(todos),
];
// If a test returns nothing it will pass by default
return store
// Dispatch async action
.dispatch(Actions.fetchTodosAsync())
// Wait for async action to complete
.then(() => {
// Mocked method is called
expect(fetchTodosMock).toHaveBeenCalled();
// Expected actions are dispatched
expect(store.getActions()).toStrictEqual(expectedActions);
});
});
});

Testing Reducers

// reducers.test.jsimport { rootReducer, initialState } from '../../reducers';
import * as Actions from '../../actions';
describe('rootReducer()', () => {
it('Handles FETCH_TODOS', () => {
const action = Actions.fetchTodos();
const state = rootReducer(initialState, action);
const expectedState = {
...initialState,
fetching: true,
};
expect(state).toStrictEqual(expectedState);
});
});

Testing Redux Saga

// sagas.test.jsdescribe('createTodo()', () => {
it('Handles success', () => {
const saga = createTodo(Actions.createTodo('Todo'));
const todo = { id: 123, text: 'Todo', done: true };
const yield1 = call(Api.createTodo, 'Todo');
const yield2 = put(Actions.createTodoSuccess(todo));
expect(saga.next().value).toStrictEqual(yield1);
expect(saga.next(todo).value).toStrictEqual(yield2);
expect(saga.next().done).toBe(true);
});
});

Integration Tests

import React from 'react';
import TestRenderer from 'react-test-renderer';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import thunk from 'redux-thunk';
import * as Api from '../../api';import { rootReducer } from '../../reducers';
import { rootSaga } from '../../sagas';
const sagaMiddleware = createSagaMiddleware();
const store = createStore(rootReducer, applyMiddleware(thunk, sagaMiddleware));
sagaMiddleware.run(rootSaga);
jest.spyOn(Api, 'fetchTodos').mockResolvedValue([
{ id: 1, text: 'Todo one', done: false },
{ id: 2, text: 'Todo two', done: false },
]);
describe('App', () => {
// Use async/await to allow for async calls to resolve
// https://reactjs.org/docs/testing-recipes.html#data-fetching
let root;
beforeAll(() => TestRenderer.act(async () => {
const renderer = await TestRenderer.create(
<Provider store={ store }>
<App />
</Provider>
);
root = renderer.root;
}));
it('Fetches todos', () => {
const todos = root.findAllByType(Todo);
expect(todos).toHaveLength(2);
expect(todos[0].props.text).toBe('Todo one');
expect(todos[1].props.text).toBe('Todo two');
});
});
it('Creates todos', async () => {
const textInput = root.findByType(TextInput);
const createTodoMock = jest.spyOn(Api, 'createTodo')
.mockResolvedValue({ id: 3, text: 'Todo three', done: false });
// Wrap each call in act() for it to take effect before the next one
await TestRenderer.act(async () => {
textInput.props.onChangeText('Todo three');
});
await TestRenderer.act(async () => {
textInput.props.onSubmitEditing();
});
const todos = root.findAllByType(Todo); expect(todos).toHaveLength(3);
expect(todos[2].props.text).toBe('Todo three');
});

Conclusion

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store