DeNA Testing Blog

Make Testing Fun, Smart, and Delighting End-Users

Bluepillを導入してiOSのUIテスト実行を並列化する

はじめまして、SWETグループの細沼(@tobi462)です。

9月から10月にかけて iOSDC 2017 や、それに関連した勉強会(リジェクトコン)などが開催され、iOS開発者にとってはホットな時期だったかと思います。私自身もiOSDC 2017ではライトニングトーク、俺コン Vol.1 / Day. 2 で15分発表をさせていただき、発表者・参加者の両面からこれらのイベントを楽しめたと思っています。

さて今回は、iOSDCでのLT発表の中で触れさせていただいたBluepillについて、実際の使い方などを掘り下げて紹介したいと思います。

サンプルコード

実際に動作するサンプルコードも用意しておきましたので、必要に応じてご利用ください。
https://github.com/YusukeHosonuma/iOS-Bluepill-Sample

Homebrewがインストールされていれば、READMEに書かれた手順に従って、実行するところまで行えるかと思います。

なおBluepillの動作確認サンプルであるため、アプリの内容に特に意味はありません。

Bluepillとは

Bluepill は、iOSアプリのUIテスト実行について、複数シミュレータを同時起動して並列で実行するツールです。

f:id:swet-blog:20171113143332p:plain

並列でテストが実行されるため、UIテストの実行時間を短縮できるというメリットがあります。また、テストは自動的に分割(グルーピング)されるため、開発者が自分でテストの分割を意識する必要がないのも便利な点といえます。

LinkedInが開発したツールで、GitHubリポジトリのREADMEによると巨大なテストスイートを妥当な実行時間で終わらせるために開発した、と書かれています。

LinkedIn created Bluepill to run its large iOS test suite in a reasonable amount of time.

また、以下のツールにインスパイアされて開発したとも書かれています。

  • parallel iOS test(pxctest)
    • 複数のOSバージョン・端末でテストを並列実行するツールで、端末カバレッジの網羅を目的としています。
  • Facebook製ツール

本記事では触れませんが、興味のある方は上記のツールも調べてみても、面白いかもしれません。

動作確認環境

以下の環境で確認を行っています。

  • Xcode 9.0(※)
  • macOS Sierra (10.12.6)
  • Fastlane 2.62.1
  • bluepill 2.0.2

ちなみにXcode 9.0にて公式で複数シミュレータが起動できるようになりましたが、それ以前のXcode 8.xの時点からBluepillは複数シミュレータ起動を実現していました。そのあたりからLinkedInのUIテストの実行時間短縮に対する本気度が伺えます。

なおREADMEにも書かれていますが、Xcode 8.xで利用したい場合は、最新バージョン(2017/11/13時点)の 2.0.2 ではなく、1.1.2 を利用する必要があるので注意しましょう。

以降、アプリやスキームの名称は xxx などに置き換えて記載していきますので、必要に応じて自身の環境に読み替えてください。

※記事執筆時点の最新バージョンは9.1ですが、Bluepillは対応中だったので9.0を利用して確認を行っています

インストール

GitHubリポジトリからダウンロード

GitHubリポジトリのリリースページでバイナリが公開されているので、そこからダウンロードしてPATHの通った適当なディレクトリ(/usr/local/bin/ など)に配置します。

解凍すると bluepillbp という2つのバイナリが格納されていますが、両方とも必須なのでご留意ください。

$ bluepill --version
Bluepill v2.0.2

Homebrewでのインストール

READMEにも書かれていますが、Homebrewでもインストールが可能なので、こちらを利用するとより手軽にインストールできます。

$ brew install bluepill
$ bluepill --version
Bluepill

ただし、(2017/11/13時点では)上記のとおりバージョン番号の出力がされないという事象があります。これはIssueにも報告が上がっていますが、しばらく動きがないようなので、気になる方はGitHubのリリースページからダウンロードした方が確実かもしれません。

設定から実行まで

以下の2段階の手順で実行します。

  1. テスト用のビルド(test-for-building)
  2. 実行(bluepill)

テスト用のビルド(test-for-building)

まずは xcodebuild コマンドの build-for-testing を利用して、テスト実行用のアプリをビルドします。

$ xcodebuild build-for-testing \
    -scheme xxxUITests \
    -destination 'platform=iOS Simulator,name=iPhone 6,OS=latest' \
    -derivedDataPath ./build
...

** TEST BUILD SUCCEEDED **

build-for-testing は、Xcode 8から導入された機能で、ビルドとテスト実行を分離できる仕組みです。通常は build-for-testing でテスト用にビルドしたアプリを test-without-building を利用してテスト実行を行います。

実行(Bluepill)

bluepill コマンドを使って実行します。

READMEには多くのオプションが記載 されていますが、必須なのは以下の2つのみです。

  • --xctestrun-path
    • .xctestrun ファイルへのパスを指定
  • --output-dir
    • 実行結果の出力先パスを指定
$ bluepill \
    --xctestrun-path \
    build/Build/Products/xxxUITests_iphonesimulator11.0-x86_64.xctestrun \
    --output-dir \
    bluepill_output

上記コマンドを実行すると、以下のようなログが出力され、デフォルト値である 並列数=4 でUIテスト実行が行われます。

Bluepill runtime version and compile time version are matched: 9.0 (9A235)
{36562} 20171029.150441 [  INFO  ] Using xctestrun configuration
{36562} 20171029.150441 [  INFO  ] This is Bluepill
{36562} 20171029.150441 [  INFO  ] Running with 4 simulators.
{36562} 20171029.150441 [  INFO  ] Packed tests into 5 bundles
{36562} 20171029.150441 [  INFO  ] Started Simulator 1 (PID 36582).
{36582} 20171029.150441 [  INFO  ] (BP-1) Running Tests. Attempt Number 1.
{36582} 20171029.150441 [  INFO  ] (BP-1) [Attempt 1] Create Simulator
{36562} 20171029.150442 [  INFO  ] 1 Simulator still running. [1]
{36562} 20171029.150442 [  INFO  ] Using 357 of 1064 processes.
{36562} 20171029.150442 [  INFO  ] Started Simulator 2 (PID 36585).
{36582} 20171029.150442 [  INFO  ] (BP-1) Booting a simulator without launching Simulator app
{36585} 20171029.150442 [  INFO  ] (BP-2) Running Tests. Attempt Number 1.
...
{36562} 20171029.150813 [  INFO  ] PID 37244 exited 0.
{36562} 20171029.150814 [  INFO  ] All simulators have finished.

またコマンドラインでオプションを指定する他にも、JSONで設定ファイルを用意する方法もあります。なお、JSONで指定するキー名では-oなどの省略系は指定できませんので注意しましょう。

{
  "xctestrun-path": "build/Build/Products/xxxUITests_iphonesimulator11.0-x86_64.xctestrun",
  "output-dir": "bluepill_output"
}

--config オプションで作成したJSONファイルを指定して実行できます。

$ bluepill --config config.json

出力結果

先ほど出力先として指定した bluepill_output フォルダの中身を見ると以下のようになっています。

bluepill_output
├── 1
│   ├── 1-xxx-results.txt
│   ├── 1-xxx-timings.json
│   ├── 1-simulator.log
│   ├── TEST-xxx-results.xml
│   └── xxx-stats.txt
├── 2
│   ├── 1-xxx-results.txt
│   ├── 1-xxx-timings.json
│   ├── 1-simulator.log
│   ├── TEST-xxx-results.xml
│   └── xxx-stats.txt
├── 3
...
└── TEST-FinalReport.xml

TEST-FinalReport.xml がJUnit形式の結果XMLファイルとなっています。Jenkinsのプラグインなど、JUnit形式をサポートしているツールで読み込ませ、見やすい形で表示することも可能です。

1から始まる数字のフォルダには、Bluepillにより自動的に分割された、各グループでのテスト結果やログなどが格納されています。あまり確認するケースは多くないかもしれませんが、覚えておくと何かあった時に調査がしやすいでしょう。

どれだけ実行時間を短縮できるか?

冒頭のスライドからの抜粋になりますが、UIテストケース数が30あるiOSアプリに対して実行したところ、それぞれの実行時間は以下のようになりました(10回施行した平均値です)

実行環境:Mac Pro (Late 2013) 3.5 GHz 6コア / 16GB

方法 実行にかかった時間 通常のテスト実行に対する比率(%)
通常 14:53 100%
Bluepill(並列数3) 9:37 65%
Bluepill(並列数4) 8:27 57%

通常のテスト実行に比べて半分以下とまではいきませんが、かなり実行時間を短縮できることが分かるかと思います。

.xctestrunの中身

ところで build-for-testing により生成され、Bluepillの実行時に指定していた .xctestrun とは何者なのでしょうか?

ファイルの実体は plist 形式になっており、XML形式で出力されています。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>xxxx</key>
  <dict>
    <key>UITargetAppPath</key>
    <string>__TESTROOT__/Debug-iphonesimulator/xxx.app</string>
    <key>TestHostPath</key>
    <string>__TESTROOT__/Debug-iphonesimulator/xxxUITests-Runner.app</string>
    <key>TestBundlePath</key>
    <string>__TESTHOST__/PlugIns/xxxUITests.xctest</string>

上記は重要と思われる部分のみを抜粋したものですが、箇条書きに直すと以下のようになります。

  • UITargetAppPath
    • __TESTROOT__/Debug-iphonesimulator/xxx.app
  • TestHostPath
    • __TESTROOT__/Debug-iphonesimulator/xxxUITests-Runner.app
  • TestBundlePath
    • __TESTHOST__/PlugIns/xxxUITests.xctest

上から順に、それぞれ「テストターゲット」「テストランナー(ホストApp)」「テストバンドル」のパスとなっています。つまり .xctestrun には、テスト実行に必要な各ファイルへのパスが記録されているということになります。

この手のツールは実際に試してみると、思わぬところでエラーが発生して利用する気を失ってしまうということがありがちですが、こういった部分を理解しておくとそうした状況で早く解決できるかもしれません。

実行オプションについて

前述したとおり、Bluepillには 多数の実行オプション が用意されています。

ここでは比較的、重要と思われるオプションを紹介します。

必須オプション

  • xctestrun-path
    • .xctestrunのパス
  • output-dir
    • ログの出力先

任意オプション

  • config
    • JSONで記述された設定ファイルへのパス
  • device
    • 実行するデバイス(デフォルト:iPhone 6
  • headless
    • ヘッドレスモードを有効にするか(デフォルト:off
    • true(※)を指定することで、シミュレータが表示されないので省メモリに繋がります
    • CI環境などでは、他Jobとの競合を避ける意味でも有効にしておくと良いでしょう
  • num-sims
    • シミュレータの並列実行数(デフォルト:4
    • Mac Pro(Late 2013)やMacBook Pro(Mid 2015)で試した限りでは、デフォルト値の 4 が安定して良い結果が得られました
    • 必要であれば、マシンスペックに応じて変更すると良いでしょう
  • screenshots-directory
    • 失敗したテストのスクリーンショットを出力するディレクトリ(デフォルト:指定なし)
    • 原因の調査がしやすくなるので、指定しておいたほうが良いでしょう
    • 相対パスで指定する場合は ../ を使用すると正しく解釈されないので注意が必要です

※README上では、デフォルト値が off と記載されているオプションがありますが true/false で指定します。

Fastlaneへの組み込み

ここまで見てきたように、Bluepillはかなり手軽に使い始めることができます。一歩進んで、Fastlaneから利用できるようにすると、さらに便利になります。

Fastlaneについての説明やインストール方法についてはここでは触れませんので、必要に応じて 公式ドキュメント などをご参照ください。

テスト用アプリのビルド(build_for_testing)

scan アクションを利用し、build_for_testingtrue を指定することで行います。

desc 'テスト実行用にビルド'
lane :build_for_testing do
  scan(
    build_for_testing: true,
    scheme: 'xxxUITests',
    destination: 'platform=iOS Simulator,name=iPhone 6,OS=latest',
    derived_data_path: './build'      
  )
end

Bluepillを用いたテスト実行(test_with_bluepill)

sh アクションを利用して、shellコマンドを前述のコマンドをそのまま実行します。

desc 'Bluepill を用いたUIテスト実行'
lane :test_with_bluepill do
  sh('bluepill -c ../config.json')
end

設定ファイルの内容は基本的に同じなのですが、パスの頭に ../ を追加している点に注意しましょう。

{
  "xctestrun-path": "../build/Build/Products/xxxUITests_iphonesimulator11.0-x86_64.xctestrun",
  "output-dir": "../bluepill_output",
  "headless": true,
  "screenshots-directory": "bluepill_screenshots"
}

これはFastlaneの実行が fastlane ディレクトリで行われるため、1つ上のディレクトリを参照しなければならないためです。ただし、前述したように screenshots-directory オプションだけは ../ を使用すると正しく解釈されないためそのままにしています。

Fastlaneプラグイン

今回は、JSONの設定ファイルを用いるアプローチを取ったのでshell実行で済ませてしまいましたが、GitHub上にいくつかBluepill用のFastlaneプラグインも公開されています。

もし要件に合うものがあれば、そちらを利用するのも手かと思います。

ただし、READMEに書かれた全てのオプションがサポートされているプラグインは無さそうでした。プラグインを利用するときには、そうした制限があることを理解した上で利用しましょう(個人的には設定ファイルに集約したほうが、メンテナンス性を上げられるかと思います)

レーンの実行

それでは用意した2つのレーンを利用して、ビルド・テストを実行してみましょう。

$ fastlane build_for_testing
...
[xx:xx:xx]: fastlane.tools finished successfully 🎉

$ fastlane test_with_bluepill
...
[xx:xx:xx]: fastlane.tools finished successfully 🎉

両方のコマンドで、Fastlaneから successfully のメッセージが出力されれば成功です。

Jenkins上で実行する

ここまで整備できていれば、Jenkins上で実行することも簡単です。

ここでは実行とテスト結果の集計方法のジョブ設定だけ紹介します(Jenkinsの基本的な使い方や設定については触れませんので、必要に応じて書籍やWeb記事をご参照ください)

ビルド・テスト実行

ビルド > ビルド手順の追加から「シェルの実行」を追加し、fastlaneコマンドを利用して先ほど用意したlaneを呼び出すようにします。

fastlane build_for_testing
fastlane test_with_bluepill

f:id:swet-blog:20171101134312p:plain

テスト結果の集計

ビルド後の処理 > ビルド後の処理の追加から「成果物を保存」と「JUnitテスト結果の集計」を追加し、bluepill_output/*.xml といったように指定します。

f:id:swet-blog:20171101134353p:plain

テスト結果の確認

正常にビルドが成功すると、ビルドの結果画面などに「最新のテスト結果」がリンク表示され、その遷移先から詳細なテスト結果を確認することが出来ます。

f:id:swet-blog:20171101134405p:plain

それぞれのテストにかかった時間も確認できるので、設定しておくと便利かと思います。

まとめ

今回は「Bluepill」についての紹介と、実際に利用するにあたってのインストール手順からFastlaneへの組み込み、Jenkinsへの設定まで紹介しました。

この手のツールは実際に利用し始めるまでに予期せぬトラブルに遭遇して、面倒になってしまって結局使わないというケースもあると思い、分かりやすい記事を心がけてみたのですがいかがだったでしょうか。

また、次回のブログ記事もお楽しみください。