I've started a Mobile App at work using React Native . Obviously this needs testing. This post is about getting started on that process.
I found a very useful
post on Medium
by Jigish Chawda that helped greatly with figuring this out.
The App
The App main interactive component is a settings page that allows you to choose your preferred language (it will, but does not yet, default to the phone's preferred out of the available 4 options and this page will allow the user to override):
The pertinent code for this screen currently looks like this:
import Languages from ' ./Languages ' ;
export default class SettingsChooseLanguage extends React . Component {
constructor () {
super ();
this . state = {
currentLanguage : Languages . DE ,
};
}
ChangeLanguage ( language ) {
this . setState ({
currentLanguage : language ,
});
}
render () {
return (
< View style = { styles . container } >
< View style = { styles . container_1 } >
< Text > Please choose your preferred language</ Text >
</ View >
< View style = { styles . container_2 } >
< TouchableOpacity
activeOpacity = { 0.5 }
onPress = { () => this . ChangeLanguage ( Languages . EN ) }
style = { styles . touchable_container } >
< Image
source = { require ( ' ./Images/UnionFlag.png ' ) }
style = {
this . state . currentLanguage === Languages . EN
? styles . flag_image_cur
: styles . flag_image
}
/>
</ TouchableOpacity >
</ View >
< View style = { styles . container_2 } >
< TouchableOpacity
activeOpacity = { 0.5 }
onPress = { () => this . ChangeLanguage ( Languages . FR ) }
style = { styles . touchable_container } >
< Image
source = { require ( ' ./Images/FrenchFlag.png ' ) }
style = {
this . state . currentLanguage === Languages . FR
? styles . flag_image_cur
: styles . flag_image
}
/>
</ TouchableOpacity >
</ View >
< View style = { styles . container_2 } >
< TouchableOpacity
activeOpacity = { 0.5 }
onPress = { () => this . ChangeLanguage ( Languages . DE ) }
style = { styles . touchable_container } >
< Image
source = { require ( ' ./Images/GermanFlag.png ' ) }
style = {
this . state . currentLanguage === Languages . DE
? styles . flag_image_cur
: styles . flag_image
}
/>
</ TouchableOpacity >
</ View >
< View style = { styles . container_2 } >
< TouchableOpacity
activeOpacity = { 0.5 }
onPress = { () => this . ChangeLanguage ( Languages . ES ) }
style = { styles . touchable_container } >
< Image
source = { require ( ' ./Images/SpanishFlag.png ' ) }
style = {
this . state . currentLanguage === Languages . ES
? styles . flag_image_cur
: styles . flag_image
}
/>
</ TouchableOpacity >
</ View >
< View style = { styles . container_1 } >
< Button
title = "Back to Settings"
onPress = { () => this . props . navigation . navigate ( ' Settings ' ) }
/>
</ View >
</ View >
);
}
}
Testing
By default React Native uses Jest for testing, and I've got no reason to use anything else.
Resolving problems
Transform react-navigation
First things first, I tried to run the test suite with the dummy test created by react-native init
. This failed:
$ npm test
> App@0.0.1 test /path/to/app
> jest
FAIL __tests__/App-test.js
● Test suite failed to run
Jest encountered an unexpected token
This usually means that you are trying to import a file which Jest cannot parse, e.g. it's not plain JavaScript.
By default, if Jest sees a Babel config, it will use that to transform your files, ignoring "node_modules".
Here's what you can do:
• To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in your config.
• If you need a custom transformation specify a "transform" option in your config.
• If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the "moduleNameMapper" config option.
You'll find more details and examples of these config options in the docs:
https://jestjs.io/docs/en/configuration.html
Details:
/path/to/app/node_modules/react-navigation/src/react-navigation.js:1
({"Object.<anonymous> ":function(module,exports,require,__dirname,__filename,global,jest){export * from '@react-navigation/core';
^^^^^^
SyntaxError: Unexpected token export
> 1 | import { createAppContainer} from 'react-navigation' ;
| ^
2 | import {createStackNavigator} from 'react-navigation-stack';
3 | import Home from './Home';
4 | import Settings from './Settings';
at ScriptTransformer._transformAndBuildScript (node_modules/@jest/transform/build/ScriptTransformer.js:537:17)
at ScriptTransformer.transform (node_modules/@jest/transform/build/ScriptTransformer.js:579:25)
at Object.<anonymous> ( AppNavigator.js:1:1)
Test Suites: 1 failed, 1 total
Tests: 0 total
Snapshots: 0 total
Time: 2.71s
Ran all test suites.
npm ERR! Test failed. See above for more details.
The fix for this is to add this "transformIgnorePatterns" code to the "jest" object in package.json
(see: Jest fails with "SyntaxError: Unexpected reserved word" everytime #256 ):
"jest" : {
"transformIgnorePatterns" : [
"node_modules/(?!(jest-)?react-native|@?react-navigation)"
]
}
N.B. I added @?
to match "@react-navigation" as well as "react-navigation"
Mocking react-native-gesture-handler
After making this change I encountered this error:
$ npm test
> App@0.0.1 test /path/to/app
> jest
FAIL __tests__/App-test.js
● Test suite failed to run
TypeError: Cannot read property 'Direction' of undefined
at Object.Direction (node_modules/react-native-gesture-handler/Directions.js:3:39)
at Object.<anonymous> ( node_modules/react-native-gesture-handler/GestureHandler.js:2:1)
Test Suites: 1 failed, 1 total
Tests: 0 total
Snapshots: 0 total
Time: 3.655s
Ran all test suites.
npm ERR! Test failed. See above for more details.
The fix for this is to add this "seutpFiles" code to the "jest" object in package.json
(see: Importing react-native-gesture-handler crashes within jest #344 and Getting Started · React Native Gesture Handler ):
"jest" : {
"setupFiles" : [ "./node_modules/react-native-gesture-handler/jestSetup.js" ]
}
And finally the initial test, which came from the generated code, passes:
$ npm test
> App@0.0.1 test /path/to/app
> jest
PASS __tests__/App-test.js (17.049s)
✓ renders correctly (1071ms)
console.warn node_modules/react-native/Libraries/Animated/src/NativeAnimatedHelper.js:249
Animated: `useNativeDriver` is not supported because the native animated module is missing. Falling back to JS-based animation. To resolve this, add `RCTAnimation` module to this app, or remove `useNativeDriver`. More info: https://github.com/facebook/react-native/issues/11094# issuecomment-263240420
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 19.131s
Ran all test suites.
Adding our tests
Making sure it renders
Based on the template App-test.js
, this just checks the component can be rendered.
import ' react-native ' ;
import React from ' react ' ;
import SettingsChooseLanguage from ' ../SettingsChooseLanguage ' ;
// Note: test renderer must be required after react-native.
import renderer from ' react-test-renderer ' ;
it ( ' renders correctly ' , () => {
renderer . create (< SettingsChooseLanguage />);
});
Testing user-interaction
To test a component with onPress
event handler (for example) we need to add a testID
property to it:
< TouchableOpacity
testID = { ' choose_en_button ' }
activeOpacity = { 0.5 }
onPress = { () => this . ChangeLanguage ( Languages . EN ) }
style = { styles . touchable_container } >
< Image
source = { require ( ' ./Images/UnionFlag.png ' ) }
style = {
this . state . currentLanguage === Languages . EN
? styles . flag_image_cur
: styles . flag_image
}
/>
</ TouchableOpacity >
Next I changed from using the 'react-test-renderer' to using the renderer from react-native-test-utils as that makes it easy to simulate events:
import ' react-native ' ;
import React from ' react ' ;
import SettingsChooseLanguage from ' ../SettingsChooseLanguage ' ;
import Languages from ' ../Languages ' ;
// Use react-native-test-utils renderer as it makes simulating events very easy
import renderer from ' react-native-test-utils ' ;
it ( ' renders correctly ' , () => {
renderer (< SettingsChooseLanguage />);
});
it ( ' no language set results in device default language ' , () => {
// TODO write me
});
it ( ' no recognised device default language falls back to English ' , () => {
// TODO write me
});
it ( ' all buttons set corresponding language ' , () => {
let button_lang_map = {
' #choose_en_button ' : Languages . EN ,
' #choose_fr_button ' : Languages . FR ,
' #choose_de_button ' : Languages . DE ,
' #choose_es_button ' : Languages . ES
};
let component = renderer (< SettingsChooseLanguage />);
for ( button in button_lang_map ) {
component . query ( button ). simulate ( ' press ' , {});
expect ( component . state (). currentLanguage ). toBe ( button_lang_map [ button ]);
}
});
it ( ' language is saved when button is pressed ' , () => {
// TODO write me
});
it ( ' re-selecting manually selected button reverts to default language ' , () => {
// TODO write me
});
Noting the place-holders for tests of functionality not yet written.