I've got 99 problems and testing RN was one
- 8+ years of consulting
- Fixing IT recruitment with TALENTED (talented.fi)
- Launched multiple native/hybrid apps with React Native
- TDD advocate, Agile Manifesto is my bible
- No idea how to test these things
- Lots of Android functionality missing
- Is there a convention for styling components?
- Android in overall quite buggy
- I got into a project via Columbia Road
- First as a fullstack developer, after personnel changes I took over the application development
- Hybrid application with React Native
- Lots of native functionality (camera, fingerprints etc)
- Huge amount of users and no room for error
export default class AAWebView extends Component {
render {
return (
<WebView
...
onNavigationStateChange={this.handleNavigationStateChange}
/>
);
}
handleNavigationStateChange ({nativeEvent}) {
if (isNotSomething(nativeEvent)) {
this.parseTitle(nativeEvent.title);
} else if (notSomethingElse(nativeEvent)) {
return webviewUtil(nativeEvent);
}
}
parseTitle (title) {
var regex = '/3462""#(/aSDMF;asnf/#(=SLAF' +
'ASfjlkn.ä''''p99090'''kl'j23"#(/))==))(/ginfa' +
'/#"(ADfsa------568-5(%_85-68-568(=€%/4079';
if (new Regex(regex).test(title)) {
titleCacheUtil(this.props.setTitle, title.match(new Regex(regex))[1]);
}
}
}
- Code really dependant on website it used
- Modified/extended React Native modules
- Only iOS had been considered
- No tests at all :(((
describe('HelloWorld', () => {
it('prints text with name', () => {
const name = 'Teemu';
const component = render(<HelloWorld name={name} />);
expect(component.text()).to.equal(`Hi ${name}`);
});
});
----
import React from ‘react’;
const HelloWorld = ({name}) => (
<div>{`Hi ${name}`}</div>
);
export default HelloWorld;
describe('HelloWorld', () => {
it('prints text with name');
});
----
import React from ‘react’;
import {Text, View} from 'react-native';
const HelloWorld = ({name}) => (
<View>
<Text>{`Hi ${name}`}</Text>
</View>
);
export default HelloWorld;
- Native modules instead of DOM elements
- One cannot use native modules in test runner
- React Native has many environmental dependencies that can be hard to simulate without a device
- Makes running tests on CI server impossible
¯\_(ツ)_/¯
- A fully mocked and test-friendly version of React Native
- Mocks RN components as normal React components
- Test like it was React, use any libraries (Enzyme!)
- Mocha + chai + sinon + enzyme is my goto-setup
- Jest is an option nowadays, not back then
// package.json
{
"name": "ReactNativeTesting",
"version": "0.0.1",
"scripts": {
"start": "react-native start",
"test": "mocha --require react-native-mock/mock.js
--compilers js:babel-core/register --recursive src"
},
"dependencies": {
"react": "15.2.1",
"react-native": "0.30.0"
},
"devDependencies": {
"babel-core": "6.11.4",
"babel-preset-react-native": "1.9.0",
"chai": "3.5.0",
"enzyme": "2.4.1",
"mocha": "2.5.3",
"react-addons-test-utils": "15.2.1",
"react-dom": "15.2.1",
"react-native-mock": "0.2.5",
"sinon": "1.17.5",
"sinon-chai": "2.8.0"
}
}
// .babelrc
{
"presets": [
"react-native"
]
}
// testutils/index.js
import React from 'react';
import chai from 'chai';
import sinon from 'sinon';
import sinonChai from 'sinon-chai';
chai.use(sinonChai);
import { shallow } from 'enzyme';
global.React = React;
global.expect = chai.expect;
global.sinon = sinon;
global.shallow = shallow;
// TestComponent.spec.js
import './testutils';
import TestComponent from './TestComponent';
describe('TestComponent', function () {
let component;
let textInput;
const defaultText = 'World';
const defaultState = {text: defaultText};
beforeEach(function () {
component = shallow(<TestComponent />);
textInput = component.find('TextInput');
});
it('has default state', function () {
expect(component.state()).to.eql(defaultState);
});
it('renders default text', function () {
const expectedText = `Hello ${defaultText}!`;
expect(component.find('Text').children().text()).to.eql(expectedText);
});
it('renders input field with placeholder', function () {
const expectedPlaceholder = 'write something';
expect(textInput).to.be.present();
expect(textInput.prop('placeholder')).to.eql(expectedPlaceholder);
});
...
});
// TestComponent.spec.js
import './testutils';
import TestComponent from './TestComponent';
describe('TestComponent', function () {
...
describe('when text changes', function () {
const newTextValue = 'teemu';
beforeEach(function () {
textInput.simulate('changeText', newTextValue);
});
it('updates component state', function () {
expect(component.state().text).to.eql(newTextValue);
});
it('renders updated text', function () {
const expectedText = `Hello ${newTextValue}!`;
expect(component.find('Text').children().text()).to.eql(expectedText);
});
});
});
// TestComponent.js
import React, { Component } from 'react';
import { Text, TextInput, View } from 'react-native';
export default class TestComponent extends Component {
state = {
text: 'World'
};
render () {
const { text } = this.state;
return (
<View>
<Text>{`Hello ${text}!`}</Text>
<TextInput
placeholder={"write something"}
onChangeText={this.handleTextChange}
value={text}
/>
</View>
);
}
handleTextChange = (text) => {
this.setState({text});
};
}
TestComponent
✓ has default state
✓ renders default text
✓ renders input field with placeholder
when text changes
✓ updates component state
✓ renders updated text
5 passing (7ms)
// TestComponent.js
...
return (
<View>
<Image source={require('./react.png')} />
<Text>{`Hello ${text}!`}</Text>
...
TestComponent
1) "before each" hook for "has default state"
0 passing (439ms)
1 failing
1) TestComponent "before each" hook for "has default state":
SyntaxError: ~/ReactNativeTesting/src/react.png: Unexpected character '�' (1:0)
> 1 | �PNG
| ^
2 |
3 |
4 | IHDR�M���tEXtSoftwareAdobe ImageReadyq�e<�5IDATx���y��S�?�{f�n�[�P�R(
// mocha-setup.js
const m = require('module');
const originalLoader = m._load;
m._load = function hookedLoader(request, parent, isMain) {
if (request.match(/.jpeg|.jpg|.png$/)) {
return { uri: request };
}
return originalLoader(request, parent, isMain);
};
// package.json
...
"scripts": {
"start": "react-native start",
"test": "mocha --require mocha-setup.js --require react-native-mock/mock.js
--compilers js:babel-core/register --recursive src"
},
...
TestComponent
✓ has default state
✓ renders default text
✓ renders input field with placeholder
when text changes
✓ updates component state
✓ renders updated text
5 passing (384ms)
// .babelrc
{
"presets": [
"react-native"
],
"env": {
"test": {
"plugins": [
"rewire"
]
}
}
}
// package.json
...
"scripts": {
"start": "react-native start",
"test": "BABEL_ENV=test mocha --require mocha-setup.js ...."
},
...
// TestComponent.spec.js
import TestComponent, { __RewireAPI__ as ReactNativeTestingAPI } from './TestComponent';
describe('TestComponent', function () {
...
let capitalizeWordStub;
...
beforeEach(function () {
capitalizeWordStub = sinon.stub();
ReactNativeTestingAPI.__Rewire__('capitalizeWord', capitalizeWordStub);
component = shallow(<TestComponent />);
textInput = component.find('TextInput');
});
...
describe('when text changes', function () {
const newTextValue = 'teemu';
const expectedTextValue = 'Teemu';
beforeEach(function () {
capitalizeWordStub.withArgs(newTextValue).returns(expectedTextValue);
textInput.simulate('changeText', newTextValue);
});
it('uses capitalizeWord utility', function () {
expect(capitalizeWordStub).to.have.been.calledWithExactly(newTextValue);
});
...
// TestComponent.js
import { capitalizeWord } from './WordUtil';
...
handleTextChange (text) {
text = capitalizeWord(text);
this.setState({text});
}
...
// TestComponent.js
...
import { capitalizeWord } from './WordUtil';
import Share from 'react-native-share';
export default class TestComponent extends Component {
...
~/ReactNativeTesting/node_modules/react-native-share/index.js:1
(function (exports, require, module, __filename, __dirname) { import React from 'react';
^^^^^^
SyntaxError: Unexpected token import
at Object.exports.runInThisContext (vm.js:53:16)
// mocha-setup.js
const m = require('module');
const originalLoader = m._load;
m._load = function hookedLoader(request, parent, isMain) {
if (request.match(/.jpeg|.jpg|.png$/)) {
return { uri: request };
}
return originalLoader(request, parent, isMain);
};
require("babel-register")({
ignore: /node_modules\/(?!react-native-share)/
});
TestComponent
✓ has default state
✓ renders default text
✓ renders input field with placeholder
when text changes
✓ uses capitalizeWord utility
✓ updates component state
✓ renders updated text
6 passing (421ms)
- 3+ months of development
- Both iOS and Android app launched successfully
- Great test coverage
- First ecommerce app in Finland where customer could actually buy stuff
- +10k downloads in couple of weeks
- Other features: bar code scanner, fingerprint login
- Each release is huge step forward (especially with fiber)
- Prepare for breaking changes
- Always use the latest
- Jest has pretty good support
See: github.com/varmais/react-native-unit-tests
Thanks!