React Native - Integration Test

昨日のUnitテストが若干結合テストを含んでいたことは置いといて, 今日はIntegration Testを説明します。

# 結合テスト

[公式ドキュメント](https://facebook.github.io/react-native/docs/testing.html#content)にもあるように現時点ではiOSのみです。このテストは、XcodeのテストとJSとのコンビネーションで実現しています。

## 環境設定

(少し前に設定したので、忘れてる部分があるかもしれません。ご容赦ください)

### 1. iOS Unit test bundleを追加

![スクリーンショット 2015-12-16 10.44.06.png](https://qiita-image-store.s3.amazonaws.com/0/13663/9b7dad90-331e-3614-a3a0-64144d4871e6.png)


### 2. Linking library

`node_modules/react-native/Libraries/RCTTest/RCTTest.xcodeproj`をリンクさせる。Xcodeの左のペインにドラックアンドドロップする。詳しくは[こちら](https://facebook.github.io/react-native/docs/linking-libraries-ios.html#content) 

*step1*
![スクリーンショット 2015-11-07 8.37.57.png](https://qiita-image-store.s3.amazonaws.com/0/13663/fc2ebb69-985d-3472-a79c-f5a0bd10cfd4.png)

*step2*
![スクリーンショット 2015-11-07 8.39.07.png](https://qiita-image-store.s3.amazonaws.com/0/13663/789cec1e-4d6f-637a-3fa7-e88f268cfa71.png)

### 3. テストファイル(*.m)を書き換える

```
#import <UIKit/UIKit.h>
#import <XCTest/XCTest.h>

#import <RCTTest/RCTTestRunner.h>

#import "RCTAssert.h"

#define RCT_TEST(name)                  \
- (void)test##name                      \
{                                       \
[_runner runTest:_cmd module:@#name]; \
}

@interface TriplentyIntegrationTests : XCTestCase

@end


@implementation TriplentyIntegrationTests
{
  RCTTestRunner *_runner;
}

- (void)setUp
{
#if __LP64__
  RCTAssert(NO, @"Tests should be run on 32-bit device simulators (e.g. iPhone 5)");
#endif

  NSOperatingSystemVersion version = [NSProcessInfo processInfo].operatingSystemVersion;
  RCTAssert((version.majorVersion == 8 && version.minorVersion >= 3) || version.majorVersion >= 9, @"Tests should be run on iOS 8.3+, found %zd.%zd.%zd", version.majorVersion, version.minorVersion, version.patchVersion);
  _runner = RCTInitRunnerForApp(@"iOS/TriplentyIntegrationTests/js/IntegrationTestsApp", nil);
}

#pragma mark Logic Tests


// This list should be kept in sync with IntegrationTestsApp.js
RCT_TEST(IntegrationTestHarnessTest)
RCT_TEST(TriplentyTest)
//RCT_TEST(AsyncStorageTest)
//RCT_TEST(AppEventsTest)
////RCT_TEST(ImageSnapshotTest) // Disabled: #8985988
//RCT_TEST(SimpleSnapshotTest)

// Disable due to flakiness: #8686784
//RCT_TEST(LayoutEventsTest)
//RCT_TEST(PromiseTest)

@end
```

### 4. JSファイルを作る

`iOS/<target test name>/js/`ディレクトリを作り、3）のRCT_TESTの引数のファイル名を作る。例にもある下記のIntegrationTestHarness.jsでテストがうまくいっているかXcodeのテストを起動して確認します。

```
'use strict';

var RCTTestModule = require('NativeModules').TestModule;
var React = require('react-native');
var {
  Text,
  View,
} = React;

var IntegrationTestHarnessTest = React.createClass({
  propTypes: {
    shouldThrow: React.PropTypes.bool,
    waitOneFrame: React.PropTypes.bool,
  },

  getInitialState() {
    return {
      done: false,
    };
  },

  componentDidMount() {
    if (this.props.waitOneFrame) {
      requestAnimationFrame(this.runTest);
    } else {
      this.runTest();
    }
  },

  runTest() {
    if (this.props.shouldThrow) {
      throw new Error('Throwing error because shouldThrow');
    }
    if (!RCTTestModule) {
      throw new Error('RCTTestModule is not registered.');
    } else if (!RCTTestModule.markTestCompleted) {
      throw new Error('RCTTestModule.markTestCompleted not defined.');
    }
    this.setState({done: true}, RCTTestModule.markTestCompleted);
  },

  render() {
    return (
      <View style={{backgroundColor: 'white', padding: 40}}>
        <Text>
          {this.constructor.displayName + ': '}
          {this.state.done ? 'Done' : 'Testing...'}
        </Text>
      </View>
    );
  }
});

IntegrationTestHarnessTest.displayName = 'IntegrationTestHarnessTest';

module.exports = IntegrationTestHarnessTest;
```

OKだったら、下記のようにコンポーネントを呼び込み下記のような感じで動かします。`TestModule.markTestPassed(true)`で成功、`TestModule.markTestPassed(false)`で失敗を呼べます。

```
'use strict';
import UserActions from 'Triplenty/app/actions/UserActions';
import UserStore from 'Triplenty/app/stores/UserStore';
import TabBarView from 'Triplenty/app/components/TabBarView';
import Triplenty from 'Triplenty/app/Triplenty';
import React from 'react-native';
var { TestModule } = React.addons;

var TriplentyTest = React.createClass({

  login(){
    let fbObj = {　... }
    UserActions.parseSignIn(fbObj);
  },

  componentDidMount() {
    this.unsubscribe = UserStore.listen(this.onUserUpdated);
    UserActions.parseLogOut();
    this.login();
  },

  onUserUpdated(){
    this.done(true);
  },

  done(success) {
    TestModule.markTestPassed(success);
  },

  render() {
    return (
      <Triplenty/>
    );
  }
});

TriplentyTest.displayName = 'TriplentyTest';

module.exports = TriplentyTest;
```


## Summary

結合テストではシミュレータが起動してテストできます。上の例ではReduxを利用していませんが、Reduxの状態とActionをうまいこと利用すれば、内部状態のテストもできると思います。

## Appendix

[appium](http://appium.io/)を利用したIntegrationテストの記事を見つけました。参考までに。

http://tech.taskrabbit.com/blog/2015/11/08/react-native-integration-tests/