react-native-test.s
- Writing tests helps the technical team to:
- Architecture its code
- Develop faster
- Prevent bugs
- Document the code
- A lack of tests will put in jeopardy the 4 points listed above.
My React Native app is well tested if:
- 1.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.
- 2.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.
- 3.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.
- 4.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.
- 5.The services are tested. It helps to not forget edge cases.
// @TODO
In these examples we use
jest
, redux-saga-test-plan
and flow
.Here is the MO to write each kind of test.
The reducer you want to test is the following:
// Reducer
const initialState = {
user: {},
books: {
favorite: {},
read: {},
toRead: {}
}
};
export default (state = {}, action) => {
switch (action.type) {
case 'SET_USER_INFO':
return {
...state,
id: action.payload.id,
name: action.payload.name,
};
default:
return state;
}
};
And the tests you will write are these ones:
//Test
it('should have no user info by default', () => {
const nextState = reducer(initialState, {});
expect(nextState.user).toEqual({})
});
it('should set the user info', () => {
const action = {
type: 'SET_USER_INFO',
payload: {
id: 1,
name: 'Donald',
},
};
const nextState = reducer(initialState, action);
expect(nextState.user).toEqual({
id: 1,
name: 'Donald',
})
});
KEY POINT: The first test is important, it allows you to check that only the 'SET_USER_INFO' action has an action on the user part of the state.
Here is the selector you want to test:
// Selector
export const userIdSelector = state => state.user.id;
And the corresponding test:
//Test
it('should select the user Id', () => {
const state = {
user: {
id: 1,
},
};
expect(userIdSelector(state)).toEqual(1);
});
Here is a saga you want to test. It makes an API call to get the user favorites books by type:
// Saga
export function* getFavoriteBooksByTypeSaga(action) {
const userId = yield select(userIdSelector);
const books = yield call(getFavoriteBookByTypeCall, userId, action.payload.type);
yield put(setFavoritesBooks(books, action.payload.type));
}
export default function*() {
yield takeEvery('GET_FAVORITE_BOOKS_BY_TYPE', getFavoriteBooksByTypeSaga);
}
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));
});
Here are the presentational component and the corresponding container we want to test:
// BookView.js - component
import type { NavigationScreenProp } from 'react-navigation';
type Props = NavigationScreenProp & DispatchProps & StateProps;
export type DispatchProps = {
setBookAsFavorite: Function,
};
export type StateProps = {
title: string,
author: string,
publicationDate?: Date,
isFavorite: boolean,
};
// BookView.container.js - container
import type { DispatchProps, StateProps } from './ChoosePlan';
const mapDispatchToProps: DispatchProps = {
setBookAsFavorite,
};
const mapStateToProps = (state: StateType): StateProps => ({
title: bookTitleSelector(state),
author: bookAuthorSelector(state),
publicationDate: bookPublicationDateSelector(state),
isFavorite: isFavoriteBookSelector(state),
});
KEY POINT: Useflow
to write this kind of 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();
});
});
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');
});
});
});
KEY POINT: You should be exhaustive in the cases.
Last modified 5yr ago