A lack of tests will put in jeopardy the 4 points listed above.
Checks
My React Native app is well tested if:
The reducers and selectors are tested. It helps to develop faster by reducing the number of manual testings. Furthermore it helps you to not forget edge cases.
The sagas, order of execution and effects on the state are tested, when the logic is not straight-forward. It prevents regressions as they hold the business logic of the app.
The props existence are tested in both containers and presentational components to ensure it's consistent. It helps to develop faster by reducing the number of manual testings.
The presentational components are tested with a snapshot. It avoids UI regression and save time when you make a change as you don't have to check all the app manually.
The services are tested. It helps to not forget edge cases.
Bad Examples
// @TODO
Good Examples
In these examples we use jest, redux-saga-test-plan and flow.
First, let's test the order of execution. NB: You're not supposed to test the order of execution of all your sagas, but only the ones with complex logic (loop, conditions, ...).
Nevertheless, for a learning purpose, we write the test for getFavoriteBooksByTypeSaga:
// Test
import { getFavoriteBooksByTypeSaga } from './sagas';
import { testSaga } from 'redux-saga-test-plan';
const favoriteCrimeBooks = [{
title: 'The Truth About the Harry Quebert Affair',
author: 'Joël Dicker'
}, {
title: 'Pars vite et reviens tard',
author: 'Fred Vargas',
}];
describe('getFavoriteBooksByTypeSaga', () => {
it('should get user favorite books and store them', () => {
testSaga(getFavoriteBooksByTypeSaga, {
type: 'GET_FAVORITE_BOOKS_BY_TYPE',
payload: { type: 'crime' },
})
.next()
.select(userIdSelector)
.next(1)
.call(getFavoriteBookByType, 1, 'crime')
.next(favoriteCrimeBooks)
.put(setFavoritesBooks(favoriteCrimeBooks))
.next()
.isDone();
});
});
KEY POINT: The test ensure that your saga has no side-effect.
A more interesting test to do is to test the saga effect on the state:
// Test
import * as matchers from 'redux-saga-test-plan/matchers';
import reducer from '../reducer';
it('should set the favorite books by type in the store', () => {
const initialState = {
user: {
id: 1,
},
books: {
favorite: {},
},
};
const expectedState = {
user: {
id: 1,
},
books: {
favorite: {
crime: favoriteCrimeBooks,
},
},
};
return expectSaga(getFavoriteBooksByTypeSaga, {
type: 'GET_FAVORITE_BOOKS_BY_TYPE',
payload: { type: 'crime' },
})
.withReducer(reducer, initialState)
.provide([
[select(userIdSelector), 1],
[matchers.call.fn(getFavoriteBookByTypeCall, 1, 'crime'), favoriteCrimeBooks],
])
.run()
.then(result => expect(result.storeState).toEqual(expectedState));
});
The props presence of a presentational component and a container (~ 5min)
Here are the presentational component and the corresponding container we want to test:
Let's say we want to take a snapshot of the BookView component of the previous part:
import 'react-native';
import React from 'react';
import BookView from './BookView';
import renderer from 'react-test-renderer';
describe('<BookView />', () => {
it('renders correctly when the book is not a favorite one', () => {
const tree = renderer.create(<BookView
navigation={() => {}}
setBookAsFavorite={() => {}}
title={'Antigone'}
author={'Jean Anouilh'}
/>
);
expect(tree.toJSON()).toMatchSnapshot();
});
it('renders correctly when the book is a favorite one', () => {
const tree = renderer.create(<BookView
navigation={() => {}}
setBookAsFavorite={() => {}}
title={'Antigone'}
author={'Jean Anouilh'}
isFavorite={true}
/>
);
expect(tree.toJSON()).toMatchSnapshot();
});
});
KEY POINT: Test the component with several sets of props. For instance if book is allowed to not have an author, make a snapshot with and one without the author name.
If a child of this component is connected, you need to mock the store in your test:
import { createStore } from 'redux';
import { Provider } from 'react-redux';
describe('<BookView />', () => {
const store = createStore(() => ({
books: {
favorite: {
crime: [],
work: [{
title: 'Lean in',
author: 'Sheryl Sandberg'
}]
},
},
user: {
name: 'Donald',
id: 1
}
}));
it('renders correctly when the book is not a favorite one', () => {
const tree = renderer.create(
<Provider store={store}>
<BookView
navigation={() => {}}
setBookAsFavorite={() => {}}
title={'Antigone'}
author={'Jean Anouilh'}
/>
</Provider>
);
expect(tree.toJSON()).toMatchSnapshot();
});
});
The services (~ 5min)
Let's say we want to test a service which formats an ISEN book code. The service is not given here, as the tests are the better explanation of what the service is supposed to do!
// FormatService.spec.js
import FormatService from './normalization';
describe('FormatService', () => {
describe('formatIsenNumber', () => {
it('should return undefined if the value is undefined', () => {
expect(FormatService.formatPhone(undefined)).to.equal(undefined);
});
it('should return undefined if length != 13', () => {
expect(FormatService.formatPhone('123')).to.equal(undefined);
expect(FormatService.formatPhone('12345678910111213')).to.equal(undefined);
});
it('should return undefined if there is a letter ', () => {
expect(FormatService.formatPhone('123a45678910')).to.equal(undefined);
});
it('should return formatted ISEN code', () => {
expect(FormatService.formatPhone('9782253002154')).to.equal('978-2-253-00215-4');
});
});
});