モバイル実機 × ブラウザ × 自動テストの構成紹介

![ドリコム Advent Calendar 2017](https://tech.drecom.co.jp/wp-content/uploads/2017/11/eyecatch_1712AdventCalendar_header.png)


これは [ドリコム Advent Calendar 2017](https://adventar.org/calendars/2575) の11日目です。
10日目はhisano1210さんによる、『[デザイナーへターミナルのすゝめ](https://tech.drecom.co.jp/designer_terminal/)』です。

## はじめに

自分の所属しているチームではブラウザゲームのフロントエンド基盤開発を行っています。基盤として提供するとなると品質は重要ですよね。
特にモバイル向けとなると実機でのパフォーマンスなどシビアに見る必要があります。
という訳で、パフォーマンスなどの品質を定量的に評価するために**【モバイル実機 × ブラウザ × 自動テスト】**の環境を構築しました。

業務で構築したものは複雑なテストや複数台の端末を使用するためなかなかヘビーな仕上がりですが、できる限り**シンプルでミニマム**な構成に整理したうえで、モバイル実機でブラウザ自動テストが行える構成をご紹介します。（使用モジュール等の詳しい説明は割愛します。インストールの項目などで公式サイトへのリンクを貼っているので、そちらをご確認ください。）

## 環境情報

### Mac
|ソフトウェア|バージョン|
|---|---|
|macOS|10.12.6 (Sierra)|
|Homebrew| 1.3.8|
|Node.js|8.4.0|
|npm|5.4.2|
|Xcode|9.1|
|Android SDK Tools|25.2.4|
|Android SDK Platform-tools|25.0.2|


### モバイル実機
|機種|OS|
|---|---|
|Galaxy S7 edge|Android 7.0|
|iPhone X|iOS 11.1.1|


## 構築手順

### 前提

 - HomebrewやNode.jsがインストールされている
 - Androidアプリがビルドできる状態である（開発者モードで動かせればOK）
 - Wildcardな開発用のProvisioningProfileでiOSアプリのビルドができる状態である
 - 環境構築するディレクトリで`npm init`などの基本的な設定が終わっている

### Appiumのインストール

モバイル用のテスト自動化OSSである[Appium](http://appium.io/)をnodeモジュールとしてインストール

```
$ npm install appium --save-dev
```

Appiumは、ブラウザテストのデファクトスタンダードであるSeleniumと同様のAPIでモバイルテストを行う事ができます。
[日本Seleniumユーザーコミュニティ](http://www.selenium.jp/)でもSeleniumと共に扱われているので、モバイルブラウザのテストを行うならAppiumを使用するのが良いと思います。


### Appiumのセットアップ

appiumの構築状況チェックツールであるappium-doctorもインストール

```
npm install appium-doctor --save-dev
```

appium-doctorを実行して構築状況を確認

```
$ node node_modules/appium-doctor/
```

下図のように全部のチェックが通るまで、指摘内容を確認しつつ設定を進める
![appium-doctor.png](https://qiita-image-store.s3.amazonaws.com/0/142643/fe162e5e-f070-f950-a547-a451ecfeeef0.png)


### Android用の追加セットアップ

特になし:smile:

### iOS用の追加セットアップ

#### iOS WebKit Debug Proxy のセットアップ

PCからモバイルSafariを操作するためのOSSである[iOS WebKit Debug Proxy](https://github.com/google/ios-webkit-debug-proxy)をインストール

```
$ brew install ios-webkit-debug-proxy
```

iOS実機にてSafariのWebインスペクタを有効にする

 ```
 設定 > Safari > 詳細 > Webインスペクタ
 ```

#### XCUITest Driverのセットアップ

appiumでXCUITestを使うための[Driver](https://github.com/appium/appium/blob/master/docs/en/drivers/ios-xcuitest.md)の依存パッケージをインストール

```
$ brew install libimobiledevice --HEAD
$ brew install ios-deploy
```

ios-deployのインストールでXcodeのコマンドラインツールが見つからないエラーが出たら下記コマンドでインストール

```
$ xcode-select --install
```

後述のテスト実行時にエラー`xcodebuild failed with code 65`が出る場合はWebDriverAgentのビルドに失敗しています（ここが一番ハマります）。下記コマンドでWebDriverAgentのXcodeプロジェクトを開いて`WebDriverAgentLib`と`WebDriverAgentRunner`に対してWildcardなProvisioningProfileなどを設定し、一度手動でビルドしてみてください（[詳しくはこちら](https://github.com/appium/appium/blob/master/docs/en/drivers/ios-xcuitest-real-devices.md))

```
$ open -a xcode ./node_modules/appium/node_modules/appium-xcuitest-driver/WebDriverAgent/WebDriverAgent.xcodeproj
```

### WebdriverIOをインストール

Node.js環境でAppiumを操作するためのOSSライブラリである[WebdriverIO](http://webdriver.io/)をインストール

```
$ npm install webdriverio --save-dev
```

WebdriverIOは[Appium公式のGetting Started](https://github.com/appium/appium/blob/master/docs/en/about-appium/getting-started.md)でも使われています。
また、[@cognitom](https://qiita.com/cognitom)さんが投稿されている[[Seleniumアレルギーのための処方箋](https://qiita.com/cognitom/items/27b7375bea653b414c8f)]でWebDriver（Selenium）クライアントの比較をされていますが、WebdriverIOが良い感じですね。


### Mochaをインストール

Node.js環境で動くテストフレームワークである[Mocha](https://mochajs.org/)をインストール

```
$ npm install mocha --save-dev
```

MochaはWebdriverIOのテストフレームワークとして公式サポートされています。
他にJasmineなども使えますが、今回は社内で使い慣れたMochaを選択しました。([参考](http://webdriver.io/guide/testrunner/frameworks.html))


### 構成まとめ

iOS用のmacOSパッケージ類

 - ios-webkit-debug-proxy
 - libimobiledevice
 - ios-deploy

Nodeモジュール類

```json:package.json
{
  "private": true,
  "dependencies": {},
  "devDependencies": {
    "appium": "^1.7.1",
    "appium-doctor": "^1.4.3",
    "mocha": "^4.0.1",
    "webdriverio": "^4.9.11"
  }
}
```

## WebdriverIOの実行

### Appiumを起動する

WebdriverIOを実行する前にAppiumを起動する

```
$ node node_modules/appium/
[Appium] Welcome to Appium v1.7.1
[Appium] Appium REST http interface listener started on 0.0.0.0:4723


```

ログがこんな感じに出たら[http://0.0.0.0:4723](http://0.0.0.0:4723)でAppiumが動いています

止める(Ctrl+C)までターミナルの1画面を占有するので、テストは別タブで実行します


### Androidで実行

AndroidのChromeでGoogle検索ページを開いてタイトルを取得するコードの例

```javascript:example.js
const wdio = require('webdriverio');

const options = {
  port: 4723, // Appiumのポート番号に合わせる
  desiredCapabilities: {
    platformName: 'Android',        // Androidで動かすことを指定
    deviceName: 'Galaxy S7 edge',   // これはなんでも良い
    browserName: 'Chrome',          // ブラウザのテストであることを指定
    automationName: 'UiAutomator2', // 実機用のテストフレームワークを指定
  }
};

wdio.remote(options).init()
.url('https://www.google.co.jp')
.getTitle().then((title) => {
  console.log('Title : ' + title);
})
.end()
.catch((error) =>{
  console.log(error);
});
```

実機をUSBでMacにつないで実行

```
$ node example.js
Title : Google
$
```

タイトルが取得できました

### iOSで実行

iOSのSafariでGoogle検索ページを開いてタイトルを取得するコードの例

```javascript:example.js
const wdio = require('webdriverio');

const options = {
  port: 4723, // Appiumのポート番号に合わせる
  desiredCapabilities: {
    platformName: 'iOS',        // iOSで動かすことを指定
    deviceName: 'iPhone X',     // これはなんでも良い
    browserName: 'Safari',      // ブラウザのテストであることを指定
    udid: '<udid>',             // 実機のUDID（これを指定すると実機で動く)
    automationName: 'XCUITest', // 実機用のテストフレームワークを指定
    xcodeOrgId: '<Team ID>',    // https://developer.apple.com/account/#/membershipのTeam ID
    startIWDP: true,            // ios_webkit_debug_proxyの自動実行を指示
  }
};

wdio.remote(options).init()
.url('https://www.google.co.jp')
.getTitle().then((title) => {
  console.log('Title : ' + title);
})
.end()
.catch((error) =>{
  console.log(error);
});
```

実機をUSBでMacにつないで実行

```
$ node example.js
Title : Google
$
```

Android同様、取得できました



## MochaとWebdriverIOを組み合わせる

### とりあえず組み合わせる

Android版でWebdriverIOとMochaを組み合わせたテストコード作成

```javascript:test.js
const wdio = require('webdriverio');
const assert = require('assert');

const options = {
  port: 4723, // Appiumのポート番号に合わせる
  desiredCapabilities: {
    platformName: 'Android',        // Androidで動かすことを指定
    deviceName: 'Galaxy S7 edge',   // これはなんでも良い
    browserName: 'Chrome',          // ブラウザのテストであることを指定
    automationName: 'UiAutomator2', // 実機用のテストフレームワークを指定
  }
};

// wdioのインスタンスを作成
const browser = wdio.remote(options);

describe('テストグループ', function() {

  // デフォルトだと2000msでタイムアウトになってしまうので延長設定
  this.timeout(60000);

  before((done) => {
    // 初期化が完了したらコールバック.
    browser.init().call(done);
  });

  after(function(){
    browser.end();
  });
  
  it('Google検索ページのタイトル取得', () => {
    return browser
    .url('https://www.google.co.jp')
    .getTitle().then((title) => {
      assert.equal(title, 'Google');
    });
  });

  it('GitHubトップページのタイトル取得', () => {
    return browser
    .url('https://github.com')
    .getTitle().then((title) => {
      assert.equal(title, 'The world\'s leading software development platform · GitHub');
    });
  });
});
```

mochaを実行する npm script として`test`を追加

```json:package.json
{
  "private": true,
  "scripts": {
    "test": "mocha test.js"
  },
  "dependencies": {},
  "devDependencies": {
    "appium": "^1.7.1",
    "appium-doctor": "^1.4.3",
    "mocha": "^4.0.1",
    "webdriverio": "^4.9.11"
  }
}
```

npm script 実行

```
$ npm run test

> @ test /Users/Hoge/...
> mocha test.js

  テストグループ
    ✓ Google検索ページのタイトル取得 (2614ms)
    ✓ GitHubトップページのタイトル取得 (5964ms)

  2 passing (44s)

$
```

テスト成功です

### 良い感じに組み合わせる

WebdriverIOのオプション定義を、共通、Android、iOSで分割

```javascript:conf.base.js
exports.wdio = {
  port: 4723
};
```

```javascript:conf.android.js
const base = require('./conf.base')

exports.wdio = Object.assign(base.wdio, {
  desiredCapabilities: {
    platformName: 'Android',        // Androidで動かすことを指定
    deviceName: 'Galaxy S7 edge',   // これはなんでも良い
    browserName: 'Chrome',          // ブラウザのテストであることを指定
    automationName: 'UiAutomator2', // 実機用のテストフレームワークを指定
  }
});
```

```javascript:conf.ios.js
const base = require('./conf.base')

exports.wdio = Object.assign(base.wdio, {
  desiredCapabilities: {
    platformName: 'iOS',        // iOSで動かすことを指定
    deviceName: 'iPhone X',     // これはなんでも良い
    browserName: 'Safari',      // ブラウザのテストであることを指定
    udid: '<udid>',             // 実機のUDID（これを指定すると実機で動く)
    automationName: 'XCUITest', // 実機用のテストフレームワークを指定
    xcodeOrgId: '<Team ID>',    // https://developer.apple.com/account/#/membershipのTeam ID
    startIWDP: true,            // ios_webkit_debug_proxyの自動実行を指示
  }
});
```

良い感じのディレクトリ構造にする

```
root/
  └─ test/ 
      ├─ conf.base.js 共通定義
      ├─ conf.android.js Android用定義
      ├─ conf.ios.js iOS用定義
      └─ spec/test.js テストコード
```

テストコードを変更し、渡された環境変数を元に定義ファイルを呼び分けるようにする

```
// 環境変数で渡された名前から対象のコンフィグをロード
const config = require(`../conf.${process.env.NODE_TEST_DEVICE}`);
// wdioのインスタンスを作成
const browser = wdio.remote(config.wdio);
```

環境変数を設定しつつmochaを実行する`test:android`と`test:ios`の npm script を記述

```json:package.json
{
  "private": true,
  "scripts": {
    "test:android": "NODE_TEST_DEVICE=android mocha ./test/spec",
    "test:ios": "NODE_TEST_DEVICE=ios mocha ./test/spec"
  },
  "dependencies": {},
  "devDependencies": {
    "appium": "^1.7.1",
    "appium-doctor": "^1.4.3",
    "mocha": "^4.0.1",
    "webdriverio": "^4.9.11"
  }
}
```

これで、Androidのテストはこんな感じで実行できます

```
$ npm run test:android
```

iOSのテスト実行はこうです

```
$ npm run test:ios
```

良い感じに**シンプルでミニマム**な構成に仕上がったんじゃないかなと思うので、[こちら](https://github.com/zprodev/appium-example/tree/master/wdio-pure)に置いておきます。

## さいごに

今回は簡単なテストを動かすところまででしたが、業務ではパフォーマンスの計測やcanvas描画の差分確認などに活用しています。是非みなさんも色々試してみてください！（とりあえずAndroidから始めるとサクッといけますよ:smile:）