RNにjestを導入してみる

まえがき

最近、ReactNativeでアプリを作っています。Reactでもテストコードを書いたことがなかったのですが、流石にまずいと思い始めたので、この機会に Jest と Enzyme を使って簡単なテストコードを書いてみようと思います。

以下は、自分が詰まったところの備忘録です。

参考

jestのテストコードは、こちらの記事を参考にしました。


環境のこと

react-native が動く環境を用意します。

$ node -v
v11.4.0
$ react-native -v
react-native-cli: 2.0.1
react-native: 0.59.8

RNプロジェクトの作成

適当な作業ディレクトリに移動します。RNのプロジェクトを作成するお決まりのコマンド。react-native init <任意のプロジェクト名>。

$ cd ws
$ react-native init trainingJest
・・・・
  Run instructions for iOS:
   • cd /Users/hoge/ws/trainingJest && react-native run-ios
   - or -
   • Open ios/trainingJest.xcodeproj in Xcode
   • Hit the Run button

 Run instructions for Android:
   • Have an Android emulator running (quickest way to get started), or a device connected.
   • cd /Users/hoge/ws/trainingJest && react-native run-android

Jestが動くか確認

Jestがきちんと動かせるかを確認するために、簡単なテストコードを書きました。

component を管理するディレクトリを分けたいので、 src ディレクトリを作ります。

src/sum.js

function sum(a, b) {
 return a + b;
}

module.exports = sum;

__test__/sum-test.js

const sum = require('./sum');

test('adds 1 + 2 to equal 3', () =>{
 expect(sum(1, 2)).toBe(3);
})

package.json

{
 "name": "trainingJest",
 "version": "0.0.1",
 "private": true,
 "scripts": {
   "start": "node node_modules/react-native/local-cli/cli.js start",
   "test": "jest"
 },
 "dependencies": {

 },
 "devDependencies": {
   "@babel/core": "7.4.5",
   "@babel/runtime": "7.4.5",
   "babel-jest": "24.8.0",
   "jest": "24.8.0",
   "metro-react-native-babel-preset": "0.54.1",
   "react-test-renderer": "16.8.3"
 },
 "jest": {
   "preset": "react-native",
   "setupTestFrameworkScriptFile": "./src/setupTest"
 }
}

"script" の中に "test": "jest" があることを確認。これが書いてることによって、npm test と打つと jest コマンドを実行してくれます。※1

いざ、テストを実行してみます!!!

$ npm test

> trainingJest@0.0.1 test /Users/hoge/ws/trainingJest
> jest

FAIL  __tests__/sum-test.js
 ● Test suite failed to run

   Cannot find module './sum' from 'sum-test.js'

   > 1 | const sum = require('./sum');
       | ^
     2 | 
     3 | test('adds 1 + 2 to equal 3', () =>{
     4 |   expect(sum(1, 2).toBe(3));

     at Resolver.resolveModule (node_modules/jest-resolve/build/index.js:230:17)
     at Object.<anonymous> (__tests__/sum-test.js:1:1)

PASS  __tests__/App-test.js

Test Suites: 1 failed, 1 passed, 2 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        5.283s
Ran all test suites.
npm ERR! Test failed.  See above for more details.

怒られました。

Cannot find module './sum' from 'sum-test.js'

とのことなので、コンポーネントのパスを確認します。どうやらテストコードがある場所からの相対パスを書くみたい。

const sum = require('../src/sum');

test('adds 1 + 2 to equal 3', () =>{
 expect(sum(1, 2)).toBe(3);
})

再実行。通った!!

$ npm test

> trainingJest@0.0.1 test /Users/hoge/ws/trainingJest
> jest

PASS  __tests__/sum-test.js
 ● Console

   console.log __tests__/sum-test.js:4
     [Function: sum]

PASS  __tests__/App-test.js

Test Suites: 2 passed, 2 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        1.231s
Ran all test suites.

ここからはcomponentのテストを書いていきます。

Componentのテストを書く

見た目がめちゃめちゃダサいですが、ログインフォームを作ってみます。まずは、コンポーネント作成。

src/LoginScreen.js

import React, { Component } from 'react';
import { StyleSheet, View, Text, TextInput } from 'react-native';
export default class LoginScreen extends Component{
 //コンストラクタ
 constructor(props){
   super(props); 
   this.state = {text: ''}
 }

 render(){

   return(
     <React.Fragment>
       <View style={styles.container}>
         <Text>ユーザー名</Text>
         <TextInput style={{height: 40, borderColor: 'gray', borderWidth: 1}} />
       </View>
       <View style={styles.container}>
         <Text>パスワード</Text>
         <TextInput style={{height: 40, borderColor: 'gray', borderWidth: 1}} />
       </View>
     </React.Fragment>
   );
 }
}

const styles = StyleSheet.create({
 container: {
   flex: 1,
   justifyContent: 'center',
   alignItems: 'center',
   backgroundColor: '#F5FCFF',
 }
});

コンポーネントができたので、テストを実行します。

  ● ReactNativeコンポーネントのテスト › 文字列に関するテスト

   Method “text” is meant to be run on 1 node. 2 found instead.

   > 44 |     const text = component.find('Text').children().text();
        |                                                    ^
     45 | 
     46 |     expect(text).toEqual(expectedText);
     47 |   });

component.find('Text')が問題でした。ログインフォームには、<Text>が2つ登場します。<Text>ユーザー名</Text>の部分と、<Text>パスワード</Text>の部分です。

要素を一つに絞り込めないとテストが Fail になるんですね〜。

とは言っても、<Text>を一つに絞ることもできないのでタグ以外で指定する方法を検討します。

公式によると

.find(selector) => ShallowWrapper

Arguments
selector (EnzymeSelector): The selector to match.

とあります。

一番簡単そうなのでCSSセレクタを使います。<Text>にid属性を追加しました。

        <View style={styles.container}>
         - <Text>ユーザー名</Text>
         + <Text id='username_text'>ユーザー名</Text>
         <TextInput style={{height: 40, borderColor: 'gray', borderWidth: 1}} />
         - <Text>パスワード</Text>
         + <Text id='password_text'>パスワード</Text>
         <TextInput style={{height: 40, borderColor: 'gray', borderWidth: 1}} />
         <LoginButton />
       </View>

テストコードを修正します。

- const text = component.find('Text').children().text();
+ const text = component.find('#username_text').children().text();

コンポーネントを変更したので、スナップショットも更新します。

$ ./node_modules/.bin/jest --updateSnapshot
● Deprecation Warning:

 Option "setupTestFrameworkScriptFile" was replaced by configuration "setupFilesAfterEnv", which supports multiple paths.

 Please update your configuration.

 Configuration Documentation:
 https://jestjs.io/docs/configuration.html

PASS  __tests__/sum-test.js
PASS  __tests__/App-test.js
PASS  __tests__/LoginScreen-test.js
› 1 snapshot updated.

Snapshot Summary
› 1 snapshot updated from 1 test suite.

Test Suites: 3 passed, 3 total
Tests:       5 passed, 5 total
Snapshots:   1 updated, 1 total
Time:        4.696s
Ran all test suites.

テストに成功しました。

挙動のテスト

ログインフォームに入力された値は、stateに保存するように変更を加えます。

          <TextInput
           id='usernameInput'
           style={{height: 40, borderColor: 'gray', borderWidth: 1}}
           value={this.state.text}
         + onChangeText={(text) => this.setState({text})}
           />

  describe('ユーザー名が入力された際の挙動のテスト', () => {

   const newTextValue = 'user_001';
   beforeEach(() => {

    usernameInput.simulate('changeText', newTextValue);
   });


   it('文字入力テスト', () => {
     expect(component.state().text).toEqual(newTextValue);
   });

 });

テストを行う。成功。

所感

導入するときは、テストコードを先行に書くことになるのでselectorで何を使うか。また、id, class名の命名規則を決めた方が良さそう。CSS以外のセレクタを今回使うことができなかったので、他も試してみたい。

終わりに

最後まで読んでくださりありがとうございます。間違いやより良いコーディングがありましたら、コメントで教えて頂けると幸いです。


※ 1 Jestコマンドをターミナルから実行しようとすると、パスが通っていなくてエラーになるので注意。下記の2つのコマンドは、同じ動きをします。

$ ./node_modules/.bin/jest
$ npm test