Reputation: 4528
How to get all tests working for Components, Redux Actions and Reducers for a Create React Native App (CRNA) using Expo (default) while not ejected?
Also uses Axios, Redux-Thunk async actions and React-Native Maps through Expo.
Upvotes: 3
Views: 902
Reputation: 4528
Well after reading and re-reading the relevant documentation for Jest, Enzyme and Redux, as well as googling issues with specific NPM package versions I sorted this out.
There's a lot of "moving parts" in that all NPM packages have to play nice together. I.E testing, mocking, redux and and your flavour of React.
Here's what works at this time (2018-01-16).
Working tests for Redux actions, reducers and components.
{
"name": "MyApp",
"version": "0.0.1",
"private": true,
"author": "Thomas Hagström <[email protected]>",
"devDependencies": {
"axios-mock-adapter": "^1.10.0",
"babel": "^6.3.26",
"babel-eslint": "^8.2.1",
"babel-jest": "^22.0.6",
"babel-polyfill": "^6.16.0",
"babel-preset-airbnb": "^1.0.1",
"babel-preset-es2015": "^6.18.0",
"babel-preset-react": "^6.16.0",
"babel-preset-react-native": "1.9.0",
"eslint": "^4.15.0",
"eslint-config-airbnb": "^16.1.0",
"eslint-plugin-import": "^2.8.0",
"eslint-plugin-jsx-a11y": "^6.0.3",
"eslint-plugin-react": "^7.5.1",
"jest": "^22.0.6",
"jest-cli": "^22.0.6",
"jest-enzyme": "^4.0.2",
"jest-expo": "^22.0.0",
"react-addons-test-utils": "^15.6.2",
"react-dom": "^16.0.0-beta.5",
"react-native-mock": "^0.3.1",
"react-native-scripts": "1.8.1",
"react-test-renderer": "^16.0.0-alpha.12",
"remotedev-rn-debugger": "^0.8.3"
},
"babel": {
"presets": [
"es2015",
"react"
]
},
"main": "./node_modules/react-native-scripts/build/bin/crna-entry.js",
"scripts": {
"start": "react-native-scripts start",
"eject": "react-native-scripts eject",
"android": "react-native-scripts android",
"ios": "react-native-scripts ios",
"test": "node node_modules/jest/bin/jest.js --watch",
"postinstall": "remotedev-debugger --hostname localhost --port 5678 --injectserver",
"eslint": "./node_modules/.bin/eslint"
},
"remotedev": {
"hostname": "localhost",
"port": 5678
},
"jest": {
"preset": "jest-expo",
"transformIgnorePatterns": [
"node_modules/(?!(react-native|jest-resolve|expo|lodash|enzyme|prop-types|react|jest-enzyme|enzyme|jest-expo|jest-serializer-enzyme|react-native-elements|react-native-google-places-autocomplete)/)"
],
"setupFiles": [
"./config/jest/globalFetch.js",
"./config/enzyme/index.js"
]
},
"dependencies": {
"@expo/vector-icons": "^6.2.2",
"axios": "^0.17.1",
"expo": "^23.0.4",
"enzyme": "^3.3.0",
"enzyme-adapter-react-16": "^1.1.1",
"lodash": "^4.17.4",
"prop-types": "^15.6.0",
"react": "16.0.0-alpha.12",
"react-native": "0.50.3",
"react-native-elements": "^0.18.5",
"react-native-google-places-autocomplete": "^1.3.6",
"react-native-maps": "^0.18.0",
"react-navigation": "^1.0.0-beta.23",
"react-navigation-redux": "^0.1.0",
"react-redux": "^5.0.6",
"redux": "^3.7.2",
"redux-logger": "^3.0.6",
"redux-promise": "^0.5.3",
"redux-thunk": "^2.2.0",
"redux-mock-store": "^1.4.0",
"remote-redux-devtools": "^0.5.12",
"socketcluster-server": "^9.1.2"
}
}
The config script for Enzyme, see package.json
below, looks like this.
// config/enzyme/index.js
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
// Setup enzyme's react adapter
Enzyme.configure({ adapter: new Adapter() });
In the root of my project I've placed mocks in a __mocks__
directory so they will automatically be picked up by Jest.
This will solve cases where native mobile API calls are used - specifically ExpoKit SDK - and not just HTTP REST.
// __mocks__/expo.js
jest.mock('expo', () => {
const expo = require.requireActual('expo');
const positionMock = {
latitude: 1,
longitude: 1,
};
// Mock the expo library
return {
Location: {
setApiKey: jest.fn(),
getCurrentPositionAsync:
options =>
new Promise(
resolve => resolve(options ? {
coords: positionMock,
} : null)
, null,
)
,
},
Constants: {
manifest: {
extra: { google: { maps: 'Your-API-KEY-HERE' } },
},
},
Permissions: {
LOCATION: 'location',
askAsync: type => new Promise(resolve =>
resolve(type === 'location' ?
{ status: 'granted' }
: null)),
},
...expo,
};
});
To configure Redux with Thunk, so you don't have to do this before every (action) test. Meaning in your tests importing redux-mock-store
will use the below implementation:
// __mocks__/redux-mock-store.js
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
export default mockStore;
Used as redux action types.
// src/Constants.js
const MapConstants = {
MAP_LOCATION_CHANGED: 'MAP REGION CHANGED',
MAP_LOCATION_BUSY: 'MAP: GETTING LOCATION',
MAP_LOCATION_SUCCESS: 'MAP: GET LOCATION SUCCESS',
MAP_LOCATION_FAILED: 'MAP: GET LOCATION FAILED',
};
Here we used the above configuration in an action test.
// src/Actions/__tests__/MapActions.test.js
import configureMockStore from 'redux-mock-store';
import { MapConstants } from '../../Constants';
import {
GetLocation
} from '../MapActions';
const store = configureMockStore();
describe('map actions', () => {
beforeEach(() => {
store.clearActions();
});
test('GetLocation returns SUCCESS when done', async () => {
const expectedPayload = { latitude: 1, longitude: 1 };
const expectedActions = [
{ type: MapConstants.MAP_LOCATION_BUSY },
{ type: MapConstants.MAP_LOCATION_SUCCESS, payload: expectedPayload },
];
// Dispatch action
await store.dispatch(GetLocation());
expect(store.getActions()).toMatchSnapshot();
expect(store.getActions()).toEqual(expectedActions);
});
});
I use a pure component and do my redux connect on a separate container.
import React from 'react';
import { shallow } from 'enzyme';
import Map from '../Map';
import { Colors } from '../../styles';
// src/Components/__tests__/map.test.js
function setup () {
const props = {
GetLocation: jest.fn(),
LocationChanged: jest.fn(),
map: {
isBusy: false,
hasError: false,
errorMessage: null,
location: null,
region: {
latitude: 45.52220671242907,
longitude: -122.6653281029795,
latitudeDelta: 0.04864195044303443,
longitudeDelta: 0.040142817690068,
},
},
};
const enzymeWrapper = shallow(<Map {...props} />);
return {
props,
enzymeWrapper,
};
}
describe('components', () => {
describe('Map', () => {
it('should render self and subcomponents', () => {
const { enzymeWrapper } = setup();
expect(enzymeWrapper).toMatchSnapshot();
const busyProps = enzymeWrapper.find('BusyIndicator').props();
expect(busyProps.isBusy).toBe(false);
expect(busyProps.loadingIndicatorColor).toEqual("#FFFFFF");
});
// TODO: Mock map functions
});
});
Ensure the reducer returns correct state and doesn't mutate it.
import MapReducer from '../MapReducer';
import { MapConstants } from '../../Constants';
describe('MapReducer', () => {
test('should return the initial state', () => {
expect(MapReducer(undefined, {}))
.toEqual({
isBusy: false,
hasError: false,
errorMessage: null,
location: null,
region: {
latitude: 45.52220671242907,
longitude: -122.6653281029795,
latitudeDelta: 0.04864195044303443,
longitudeDelta: 0.040142817690068,
},
});
});
test(`should handle ${MapConstants.MAP_LOCATION_BUSY}`, () => {
expect(MapReducer({}, {
type: MapConstants.MAP_LOCATION_BUSY,
}))
.toEqual({
hasError: false,
errorMessage: null,
isBusy: true,
type: MapConstants.MAP_LOCATION_BUSY,
});
});
test(`should handle ${MapConstants.MAP_LOCATION_SUCCESS}`, () => {
const resultArray = ['test'];
expect(MapReducer({}, {
type: MapConstants.MAP_LOCATION_SUCCESS,
payload: resultArray,
}))
.toEqual({
isBusy: false,
hasError: false,
errorMessage: null,
location: resultArray,
type: MapConstants.MAP_LOCATION_SUCCESS,
});
});
test(`should handle ${MapConstants.MAP_LOCATION_FAILED}`, () => {
const errorString = 'test error';
expect(MapReducer({}, {
type: MapConstants.MAP_LOCATION_FAILED,
payload: errorString,
}))
.toEqual({
isBusy: false,
hasError: true,
errorMessage: errorString,
location: null,
type: MapConstants.MAP_LOCATION_FAILED,
});
});
test(`should handle ${MapConstants.MAP_LOCATION_CHANGED}`, () => {
const resultArray = ['test'];
expect(MapReducer({}, {
type: MapConstants.MAP_LOCATION_CHANGED,
payload: resultArray,
}))
.toEqual({
isBusy: false,
hasError: false,
errorMessage: null,
region: resultArray,
type: MapConstants.MAP_LOCATION_CHANGED,
});
});
});
Upvotes: 5