AngularJS + Cordova なプロジェクトに Protractor + Appium を導入して iOS シミュレータで E2E テストを動かす

タイトルが長くなってしまったが、

という流れである。

Protractor は AngularJS 向けに作られた E2E テストツール。

一方 Appium は Selenium WebDriver の一種で、E2E テストツールと WebDriver の間に割り込んで、iOS シミュレータや iOS 実機を操作してくれる。細かな仕組みは厄介なので、とりあえず「iOS シミュレータで E2E テストする時に使う Selenium の亜種」ぐらいに思っておけばよいかと。

iOS シミュレータの動作、および Appium の動作のために色々とインストールが必要なモノが多いので、それらをまとめて紹介する。

実際のコードはコチラ

今回紹介したパッケージとサンプルコードを利用し、AngularJS + Cordova なアプリで Appium を使った E2E テストを行うサンプルプロジェクトを作成した。以下を参考にしてほしい。

XCode のライセンスに同意する

AppStore から XCode をインストールした後、コマンドラインでライセンスに同意しないといけない。

$ sudo xcodebuild -license

このコマンドを叩くと何やら文章が出てくる。Space キーで抜けたら「agree」と入力して Enter することでライセンス認証ができる。

Homebrew でインストールするモノ

Homebrew より

をインストールする。

Lib iMobile Device は iOS WebKit Debug Proxy が依存しているのか、一緒にインストールされるが、最新版 (--HEAD) にアップデートしないと上手く動かないので更新をかけておく。

$ brew install carthage ios-webkit-debug-proxy
$ brew upgrade libimobiledevice --HEAD

# これで以下3つが追加されていれば OK
$ brew list
carthage                # Cathage : appium-xcuitest-driver が依存している
git
ios-webkit-debug-proxy  # iOS WebKit Debug Proxy : Safari インスペクタでデバッグする際に使用する
libimobiledevice        # Lib iMobile Device : USB 接続された iOS デバイスの情報を取得するモノ

RubyGems でインストールが必要なモノ

RubyGems で XC Pretty というツールを入れておく。

$ gem install xcpretty

# これで以下2つがインストールされていれば OK
$ gem list
*** LOCAL GEMS ***
rouge (2.0.7)     # Rouge : XCPretty が依存しているシンタックスハイライタ
xcpretty (0.2.8)  # XCPretty : XCode のログを整形して表示する

npm でグローバルインストールが必要なモノ

最後に npm でグローバルインストールしておくモノたち。

# グローバルインストールする
$ npm install -g appium-doctor authorize-ios deviceconsole ios-deploy ios-sim

# iOS の認証を行う
$ sudo authorize-ios

# Appium が動作する環境になっているかチェックする
# 何か問題があれば別途確認する
$ appium-doctor --ios

# 以下がインストールされていれば OK
$ npm list -g
├── appium-doctor@1.4.2  # Appium Doctor : Appium が使える環境かチェックする
├── authorize-ios@1.0.5  # Authorize iOS : iOS の認証を行う
├── deviceconsole@1.0.1  # Device Console : iOS のログを整形する
├── ios-deploy@1.9.1     # iOS Deploy : XCode を経由せず iOS アプリをインストールするために使用する
├── ios-sim@6.0.0        # iOS Sim : XCode を経由せず iOS アプリを起動するために使用する

これで Mac 環境全体で用意しておくモノは終わり。

プロジェクトに Protractor と Appium をインストールする

いよいよプロジェクトの準備。Protractor と Appium、そしてその連携に必要な WD (WebDriver) と WD Bridge というツールを入れる。

$ npm i -D protractor wd wd-bridge appium

今回は Gulp スクリプトから Protractor を実行したいのと、Appium サーバの起動も Gulp スクリプトに定義したいため、以下も入れておく。Jasmine-Spec-Reporter は Protractor の実行結果を整形して出力するためのモノ。

$ npm i -D gulp-protractor gulp-shell jasmine-spec-reporter

Appium の設定ファイルを作る

Appium はサーバとして起動させるため、その設定ファイルを用意する。nodeConfig.json という名前でプロジェクト直下に以下のようなファイルを作る。

{
  "configuration": {
    "cleanUpCycle": 2000,
    "timeout": 30000,
    "proxy": "org.openqa.grid.selenium.proxy.DefaultRemoteProxy",
    "url": "http://127.0.0.1:4723/wd/hub",
    "host": "127.0.0.1",
    "port": 4723,
    "maxSession": 1
  }
}

何か色々な文献を見て研究した結果、こういう設定ファイルに落ち着いたけど、何が必要で何が不要かもう忘れてしまった。大体はデフォルト設定だと思うのだが、これでいいと思う。

続いて gulpfile.js に Appium サーバを起動するタスクを作成する。

const gulp = require('gulp');
const $ = require('gulp-load-plugins')();

gulp.task('appium', $.shell.task(
  ['appium --nodeconfig ./nodeConfig.json --session-override'],
  { quiet: false }
));

要するにコンソールで $ appium --nodeconfig ./nodeConfig.json --session-override と打てば良いだけなのだが、コレを $ gulp appium で呼べるようにするために Gulp-Shell を使っている。

Protractor の設定ファイルを用意する

続いて Protractor の設定ファイルの用意。protractor.conf.js なんて名前でプロジェクト直下にファイルを作って、以下のように書く。

exports.config = {
  // Appium サーバの URL
  seleniumAddress: 'http://127.0.0.1:4723/wd/hub',
  
  // Base URL : 未指定で OK
  baseUrl: '',
  
  // Capabilities
  capabilities: {
    // Browser Name : Cordova アプリの場合は空にしておく
    browserName: '',
    // Auto WebView : AngularJS などハイブリッドアプリの場合は true にしておく
    autoWebview: true,
    
    // App : アプリファイルのパスを指定する
    app: './platforms/ios/build/emulator/【アプリ名】.app',
    // Bundle ID : config.xml に記載のアプリの識別子を指定する
    bundleId: '【アプリの識別子】',
    
    // Device Name : iOS シミュレータで E2E テストをする場合はシミュレータのデバイスを指定する
    deviceName: 'iPhone 7',
    // UDID : iOS 実機で E2E テストをする場合に使用する。今回は割愛
    // udid: '',
    
    // Platform Name : 「iOS」で OK
    platformName: 'iOS',
    // Platform Version : XCode でインストールしている SDK のバージョンに合わせておく
    platformVersion: '10.3',
    
    // Full Reset : iOS シミュレータの場合に起動する度にアプリを再インストールするか否かの設定
    fullReset: false,
    // Auto WebView Timeout : タイムアウトの設定
    autoWebviewTimeout: 20000,
    // Auto Accept Alerts : 認証系のアラートを自動的に許可する
    autoAcceptAlerts: true
  },
  
  onPrepare: () => {
    // Jasmine Spec Reporter の設定 (テスト結果をコンソールにイイカンジに出力する)
    const SpecReporter = require('jasmine-spec-reporter').SpecReporter;
    jasmine.getEnv().addReporter(new SpecReporter({
      spec: {
        displayStacktrace: true,
        displayPending: false
      },
      summary: {
        displayPending: false
      }
    }));
    
    // Implicitly Wait
    browser.manage().timeouts().implicitlyWait(20000);
    
    // この設定が謎。環境によっては false にしないと Click 等の動作を待たなくなる場合もあったが
    // 検証時は true にしておかないとアプリが正しく動作してくれなかった
    browser.ignoreSynchronization = true;
    
    // Wd (Web Driver)・Wd Bridge の設定
    const wd = require('wd');
    const protractor = require('protractor');
    const wdBridge = require('wd-bridge')(protractor, wd);
    wdBridge.initFromProtractor(exports.config);
    
    // Cordova アプリ向けに URL の表記を http プロトコルから file プロトコルに直す
    const defer = protractor.promise.defer();
    browser.executeScript('return window.location;')
      .then((location) => {
        browser.resetUrl = 'file://'
        browser.baseUrl = location.origin + location.pathname;
        defer.fulfill();
      });
    return defer.promise;
  },
  
  //  Spec ファイルの指定
  specs: ['./e2e/**/*.spec.js'],
  
  // Jasmine Node の設定
  jasmineNodeOpts: {
    showColors: true,
    defaultTimeoutInterval: 500000,
    includeStackTrace: true,
    isVerbose: true,
    print: () => {}
  }
};

長ったらしいが、この設定に落ち着くのに時間がかかった。

特に browser.ignoreSynchronization の設定が難儀で、アプリによって true じゃないとダメだったり、false じゃないとダメだったりでよく分からない。AngularJS を HTML に埋め込むやり方の違いによるものかなと思慮。

あと Gulp スクリプトから Protractor を起動するためのタスクを作る。

gulp.task('e2e', (done) => {
  gulp
    .src('./e2e/**/*.spec.js')
    .pipe($.protractor.protractor({
      configFile: 'protractor.conf.js'
    }))
    .on('error', (error) => {
      throw error;
    })
    .on('end', () => {
      done();
    });
});

これは単純に Gulp-Protractor から Protractor を実行するだけ。

E2E テストコードを書いてみる

では実際にこの設定で Protractor と Appium を動かすため、簡単な E2E テストコードを書いてみる。

テスト対象とするのは AngularJS の公式ページのトップにある、「テキストボックスに文字列を入力すると、その文字列が h1 要素に表示される」というモノ。実コードは GitHub で見てみてください。

テストコード e2e/index.spec.js はこんな感じ。

describe('トップページ', () => {
  beforeEach(() => {
    // トップページを開き直す
    browser.get('');
  });
  
  it('名前を入れると表示される', () => {
    // テキストボックスに文字を入力する
    element(by.id('your-name')).sendKeys('My Name');
    // h1#message に入力した文字を含めたテキストが出力されているべき
    expect(element(by.id('message')).getText()).toBe('Hello My Name!');
    // 目視で iOS シミュレータの動作を見てみたいのでココでは2秒停止させてみた
    browser.sleep(2000);
  });
});

Protractor は Jasmine ベースでテストコードが書ける。細かな書き方は別途 Protractor の使い方を調べてみてほしい。

実際に E2E テストを行ってみる

ようやく E2E テストの実施にこぎつけた…。

予め Cordova アプリをビルドしておく。実際にテストする iOS シミュレータでアプリが起動するところまでやっておくと良いだろう。

# 何らかのビルドタスクで www ディレクトリに成果物を作り…
$ npm run build
# www ディレクトリのブツを Cordova ビルドして iOS シミュレータにインストールする
$ npm run cordova emulate --target=iPhone-7

アプリの準備ができたら、まず1つ目のターミナルで、Gulp の appium タスクを実行する。

$ npm run gulp appium

これで Appium サーバが立ち上がるので、このターミナルはこの状態で放っておく。

次に、2つ目の別のターミナルを開き、コチラで Gulp の e2e タスクを叩いて Protractor を実行する。

$ npm run gulp e2e

e2e タスクを実行すると iOS シミュレータが起動し、iOS 上で操作を再現するための WebDriver アプリが自動的にインストールされる。その後テスト対象の Cordova アプリが開き、E2E テストが開始される。

結果は e2e タスクを実行したコンソールに表示される他、iOS シミュレータとの接続状況は appium タスクを実行したコンソールにも随時表示されている。


これにて Protractor + Appium による、AngularJS + Cordova アプリの E2E テストが出来た。

iOS 実機でも E2E テストを実行することは可能だが、ローカルにインストールした appium に対して追加で設定が必要だったりしてそこが難儀なので、別途紹介する。