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):

Language Selection Screen screenshot

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.