DeNA Testing Blog

Make Testing Fun, Smart, and Delighting End-Users

SWETの2名が執筆した「iOSアプリ開発自動テストの教科書」が発売されました

こんにちは、SWETの細沼(@tobi462)です。

iOSアプリ開発における自動テストをテーマとしたiOSアプリ開発自動テストの教科書が6/27(木)に発売されました! 私と同じくSWETの平田(@tarappo)の2名で共著しています。

gihyo.jp

iOSアプリ開発自動テストの教科書〜XCTestによる単体テスト・UIテストから、CI/CD、デバッグ技術まで

iOSアプリ開発自動テストの教科書〜XCTestによる単体テスト・UIテストから、CI/CD、デバッグ技術まで

発売を記念して、本書が生まれた背景や各章の見どころについて、かんたんに紹介させていただきます。 f:id:swet-blog:20190718173518j:plain

企画の立ち上がり

本書の企画が持ち上がったのは2017年6月と、今からおよそ2年前のことになります。

SWETでは「iOS Test Night」という勉強会を以前から開催していましたが、 それに参加された技術評論社の方から「iOSテスト本を執筆しませんか?」という提案を受けたのがきっかけでした。

その当時(および現在でも)、iOSアプリ開発についての入門書は多く出版されていたものの、 テストやCI/CD・デバッグテクニックといった少し進んだテーマを扱った書籍はありませんでした。

iOS Test Nightでは、テストについてのナレッジ共有を主目的としており、それは一定数の成果を収めていたと思います。 しかし、あくまで断片的な情報にとどまっており、体系化されたナレッジとしては整っていないという課題感を持ち続けていました。

それが書籍という形で体系化され世の多くのエンジニアに知ってもらえることには十分な価値があると考え、本書の執筆を決めました。

本書の構成

本書は、5パートで構成されています。

  • 第1部:自動テストについて
  • 第2部:単体テスト
  • 第3部:UIテスト
  • 第4部:CI/CD
  • 第5部:デバッギング

執筆の開始当初は、テストにまつわるTIPSをまとめた、いわゆる「TIPS本」のようなものを考えていました。 しかし執筆作業が進むにつれ、それでは体系化された知識を提供するのは難しいという判断に至り、 一から体系的に学べるような構成に大幅に変更となった経緯があります。

執筆途中での構成変更はとても大変な作業でしたが、 結果としてよりよい書籍に仕上がったと感じています。

本書の対象読者

本書籍では、自動テストについて初歩から体系的に学べるような内容となっており、主に以下のような読者を対象としています。

  • 自動テストをほとんど(あるいは全く)書いたことがない方
  • CI/CDサービスをこれから導入したいと思っている方

一言でいえば「初心者」を主な対象読者としていることになりますが、 一部では進んだテクニックについても解説を試みており、 「中級者」以上の方でも何かしら得られる内容に仕上がっているかと思います。

実際に購入すべきかの判断については、ぜひ書店などで立ち読みして判断いただければ幸いです!

各パートの見どころ

ここからは各パートの見どころについて紹介していきます。

第1部:自動テストについて

第1部では、自動テストを実装・運用するにあたり知っておいたほうが良いことについて記載しています。

  • 1章:自動テストをはじめる前に
  • 2章:自動テストにおける落とし穴を避ける

1章では、自動テストをはじめる前に知っておくと良い「自動テストを実装する目的」や「自動テストを実装する際に意識するべきこと」などについて説明をしています。

また、2章では「テストケース名の話」から、「実行時間をどうするかといった話」といった自動テストを利用していく上で落とし穴になりえるであろうことについて説明をしています。

事前にこれらのことを知っておくことで、より自動テストを活用できると思います。

第2部:単体テスト

第2部では、XCTestの基本から実践的なテクニック、およびOSS活用について記載しています。

  • 3章:XCTestを利用した単体テスト
  • 4章:単体テストに役立つOSSを活用する

3章のXCTestでは基本的な利用方法から始まり、標準のAPIをほぼ網羅するように解説しています。 その上で、独自アサーションの書き方や非同期APIのテストについて、初心者でもわかりやすいように丁寧な説明を心がけました。 また、テストが書きづらい場合の対処など、より実践的なケースについても軽く触れています。

4章では、以下の4つのOSSについて解説を行っています。

考え方が異なるOSSの解説を通じて、様々なテストテクニックを学んでもらえることを狙いとしました。 これらの軸を知ることで、流行などに振り回されないOSS選定ができるようになれば幸いです。

第3部:UIテスト

第3部では、XCUITestを利用したUIテストについて記載しています。

  • 5章:XCTestを利用したUIテストの基本
  • 6章:XCUITestのAPIを理解する
  • 7章:UIテストの一歩進んだテクニック

XCUITestを用いたUIテストの基本から、提供されているAPIの使い方についてある程度網羅的に解説しています。 また、より実践的なテストコードの書き方についても軽く触れています。

これらの章を通じて、XCUITestでどのようなことができるのか分かって貰えればと思います。

第4部:CI/CD

第4部では、CI/CDについて記載しています。

  • 8章:CI/CDの基本を押さえる
  • 9章:fastlaneを利用したタスクの自動化
  • 10章:アプリ配信サービスとデバイスファームの活用
  • 11章:BitriseとCircleCIによるパイプラインの自動化

本パートでは、CI/CDとはという説明にはじまり、実際のサービスの利用例を中心に紹介しています。

デバイスファームについてはまだ利用されてない方も多いと思います。 本パートの情報により、実際に触ってみる一助になれば幸いです。

第5部:デバッギング

第5部では、iOSアプリ開発における様々なデバッグテクニックを紹介しています。

  • 12章:デバッグのテクニック

デバッグツールは使いこなせば強力なものですが、なかなか学ぶ機会がありません。 本書ではブレークポイントやLLDB、Xcodeに用意されたデバッグ機能などについて、初心者でも分かりやすいような解説を心がけました。

明日から使えるテクニックばかりなので、日々の生産性向上につながればとても幸いです。

おわりに

今回は、SWETにおける「テスト自動領域」のiOSを担当する「細沼」と「平田」で共著した iOSアプリ開発自動テストの教科書について、 執筆することになったきっかけや見どころについて解説させていただきました。

今後、社内でiOSのテストに関するハンズオンなどを開催していく予定ですが、それの教材や題材作成の元ネタとして、我々自身もこの書籍を活用していく予定です!

iOSアプリ開発は、Androidアプリ開発と違い、公式ドキュメント・サンプルコードが少なく苦労することも多いと思います。 とくに自動テストについてのドキュメントは少なく、簡素なAPIドキュメントをもとに試行錯誤する現状も多いはずです。

本書がそうした現状を改善する、ひとつのきっかけとなれば幸いです!

SWETグループが考える形式手法の現在とこれからの可能性

こんにちは、SWETの鈴木穂高(@hoddy3190)です。

私は、こちらの記事で紹介されているようなAndroidテストの教育活動をする傍ら、形式手法という技術の可能性を模索しています。 今回は、形式手法についての簡単な説明や、調べていくにつれてわかってきた実用可能性等をご紹介できればと思います。

動機

まず、なぜ私が形式手法について調べようと思ったのかをご説明します。

SWETに所属する前、私は、別の部署で4年ほどゲーム開発に携わっていました。
そこでよく課題に感じていたのは、日本語で書かれる仕様の不備(考慮漏れ、記載漏れ、矛盾など)により、大きな手戻りにつながることが多いということでした。
開発プロセス上でそのような問題が発生すると、当然再発防止策MTGが開かれます。 有識者のレビューを開発フェーズのより早い段階で組み込むようにするフロー改善や、 考慮漏れを防ぐためのチェックリストなどがよく再発防止策として施行されます。

しかし、プロダクト特有の属人性の高い知識が求められるレビューやチェックリストの目視チェックは、段々と運用の綻びが出てきます。 再発防止策として全く機能しないとまでは言いませんが、策の運用が崩れる様子を何度も目にしてきた中で、 技術的なアプローチでこれらを解決できないかを考えるようになりました。

そこで出会ったのが、形式手法という技術です。

形式手法とは

形式手法とは、仕様を明確に記述したり、記述された設計の性質を機械的に検証する手法の総称です。 形式手法にもいくつか種類がありますが、いずれも数学に基づく科学的な裏付けを持ちます。

種類 説明 代表的な記述言語
形式仕様記述 矛盾がなく論理的に正しい仕様を作成する VDM++/Event-B/Z/Alloy etc.
モデル検査 プログラムの状態をモデル化することで、プログラムが期待される性質を満たすことを検証する Promela/TLA+ etc.
定理証明 法則や説明に基づき、理論的に性質が成り立つことを示していく Coq/Isabelle etc.

形式手法という言葉を聞いたことがないという方も多くおられると思いますが、 実は形式手法自体の研究は1970年ごろから始まっており、その歴史は深いです。

日本ではまだあまり多くの導入事例は公開されていはいませんが、 形式手法を適用して品質向上につながったという事例は、欧米を中心に多く報告されています。

導入事例としてどのようなものがあるのかは、形式手法の実践ポータルがよくまとまっていると思います。 航空宇宙や鉄道など、高い信頼性が必要とされる分野での事例が多いです。 IT業界でも、AmazonのAWSに導入されているという事例はありますが、 公開されている事例は多くありません。

私はまず、形式仕様記述とモデル検査について、調べてみたり、実際に触ってみたりして、前述した仕様書の問題への解決に活用できそうかを検討しました。 その結果、形式仕様記述、モデル検査ともに、開発へ適用する際のコストの高さや、仕様書問題への解決策として本当に効果的かという点に懸念が出ました。

しかし、モデル検査においては、設計や実装の考慮漏れを防げる可能性がありそうだということがわかってきました。 早くも動機の部分とずれが生じてしまいましたが、形式手法の可能性をさらに深掘る投資の価値はありそうだということで、 モデル検査についてさらに詳しく調べてみることにしました。 なお、仕様書問題に対しても、現在、形式仕様記述とはまた別のアプローチの解決策を模索していますが、今回は触れません。

モデル検査について

モデル検査とは、システムを有限個の状態を持つモデルで表現し、モデルが取りうるすべての状態を機械的かつ網羅的に検査することで、システムが仕様を満たすことを確認する手法です。

例えば、1から3の整数値を取りうる変数a, bがあったとし、「aとbの積が常に9以下である」が成り立つのかを確認したいとしましょう。 モデル検査の手順としては、次のようになります。

  1. 検査したいもの(仕様書、ソースコードなど)から専用のモデリング言語でモデルを作成する
  2. 検査対象が満たすべき性質から検査式を作成する
  3. モデル検査ツールにかける

モデルを作成すると下のようになります。検査式は、assertから始まる部分になります。 モデル検査のモデルを書くための言語にはいくつか種類があるのですが、ここではPromelaという言語で記述しています。

sample.pml

inline Choose(n) {
    // 他のプログラミング言語でよく見るif文とは意味が異なり、
    // "::"で始まる3つの文のうち、非決定的に(ランダムに)実行される
    // 網羅的な検査をするときは、ここで取りうるすべてのパターンが探索される
    if
    :: n = 1
    :: n = 2
    :: n = 3
    fi
}

active proctype P() {
    int a = 0, b = 0
    Choose(a) // 上で定義しているChooseがインライン展開され、aには1から3のいずれかの値が代入される。
    Choose(b)
    assert(a * b <= 9)
}

モデル検査ツールにはSpinというツールを使います。 コマンドラインで検査にかけることができます。

$ spin -a -o2 ./sample.pml # spinはHomebrewでインストール可能
$ gcc -o pan pan.c
$ ./pan -c0 -e
$ spin -pglsr -t1 sample.pml   # 結果出力

検査のロジックとしては、 (a, b) = (1, 1), (1, 2), ...,(3, 2), (3, 3)と 取りうるすべての組み合わせを網羅的に探索した上で、a * b <= 9を満たすことを確認します。 モデルに一切そういうロジックを書かずに、このような網羅的な探索ができるのも、モデル検査の良いところです。

では、検査式をassert(a * b < 9)に置き換えるとどうなるでしょう。 その場合、a * b < 9を満たさなかったaとbのすべての組を反例として示してくれます。

using statement merging
  1:    proc  0 (P:1) a.pml:5 (state 3) [a = 3]
  2:    proc  0 (P:1) a.pml:5 (state 9) [b = 3]
spin: a.pml:13, Error: assertion violated
spin: text of failed assertion: assert(((a*b)<9))
  2:    proc  0 (P:1) a.pml:13 (state 13)   [assert(((a*b)<9))]
spin: trail ends after 2 steps
#processes: 1
  2:    proc  0 (P:1) a.pml:14 (state 14) <valid end state>
1 process created

(a, b) = (3, 3)のときに、検査式を満たさなかったことがわかります。

このように、モデル検査は、自動的に網羅的な検査をしてくれます。
そのため、並列システムの不整合のような再現させづらいタイミングで発生する不具合や、場合分け不足など設計の考慮不足が起因の不具合などを効果的に見つけてくれます。 品質の高いソフトウェアを開発するための有効な手段の1つになるだろうと考えています。

一方、モデル作成の難易度が高いというデメリットもあります。 プログラミング言語とは少し違うモデリング言語独特の文法で記述しないといけないことに加え、 取りうる状態が膨大になればなるほど検査にかかる時間が増える(状態爆発)こともよくあり、 実際のシステムをうまく抽象化してモデルを作成するスキルも求められます。

ここまでの内容に関して、詳しくは、 私が2019/03/07のAndroid Test Night#6で発表した、「形式手法について調べてみた」に掲載しております。お時間があれば是非ご覧になってください。

PoCづくり

モデル検査の仕組みがわかったところで、プロダクトに適用できるのかどうかを確かめるために、いくつかの簡単なPoCを作ってみることにしました。 PoCとは、Proof of Conceptの略で、ここではモデル検査の実用性を確かめるために、簡単なデモを作ることを指しています。 PoCの題材決めに際しては、プロダクトに適用する可能性も加味し、プロダクト開発でよく起きる設計・実装上の問題にフォーカスすることを心がけました。

そこで、テーマとして挙げたのは、MySQLです。 MySQLは、トランザクション分離レベルごとの挙動の把握が難しく、障害につながることがよくあると思います。 今回はMySQLにおけるdeadlockの検知をするモデルのご紹介をしたいと思います。

MySQLのlockの仕組みをかなり簡略化したモデルを作成し、 複数のプロセスから、CRUDのクエリをランダムにそのモデルに投げ、異常が起きるかどうかを検査します。

/**
 *  MySQL 5.6
 *  REPEATABLE READ
 */

mtype {P1, P2}         // MySQLにクエリを発行するトランザクションの識別子
bool gap_lock_by_P1[6] // 共有ロック
bool gap_lock_by_P2[6] // 共有ロック

/**
 *
 *    negative infinity
 *  ---------------------
 *           |
 *           | gap0
 *           |
 *    +--------------+
 *    |    record    |
 *    +--------------+
 *           |
 *           | gap1
 *           |
 *    +--------------+
 *    |    record    |
 *    +--------------+
 *           |
 *           | gap2
 *           |
 *    +--------------+
 *    |    record    |
 *    +--------------+
 *           |
 *           | gap3
 *           |
 *  ---------------------
 *    positive infinity
 *    
 */

// row_num は select or update or deleteの検索対象(スキャン対象ではない)
inline lock_gap(proc_id, row_num) {
    if
    :: proc_id == P1 -> gap_lock_by_P1[row_num] = true
    :: proc_id == P2 -> gap_lock_by_P2[row_num] = true
    fi
}

// gap lockをかける処理であれば、実際はupdateでもdeleteでも良い
inline select_for_update(proc_id, row_num) {
    lock_gap(proc_id, row_num) // gap lockを起こす
}

inline insert(row_num) {
    // gap_lock_by_P1[row_num]とgap_lock_by_P2[row_num]が共にfalseになるまで待つ
    !gap_lock_by_P1[row_num] && !gap_lock_by_P2[row_num]

    // データ挿入は今回検査したい対象ではない
    // 書いても状態数を増やすことにつながるため書かない
}

inline commit(proc_id, row_num) {
    if
    :: proc_id == P1 -> gap_lock_by_P1[row_num] = false
    :: proc_id == P2 -> gap_lock_by_P2[row_num] = false
    fi
}

// トランザクション内の処理
inline exec(proc_id, row_num) {
    select_for_update(proc_id, row_num); insert(row_num); commit(proc_id, row_num)
}

proctype P(mtype proc_id) {
    // P1, P2はMySQLへの操作を、非決定的に繰り返し行う
    do
    :: exec(proc_id, 0)
    :: exec(proc_id, 1)
    :: exec(proc_id, 2)
    :: exec(proc_id, 3)
    od
}

// 一番最初に呼ばれる処理
init {
    atomic { // atomicで囲まれた部分は不可分処理となる(`run P(P1)`と`run P(P2)`の間に、他の処理がinterruptしないことが保証される)
        // 同じ処理を行うプロセスP1、P2を起動する
        run P(P1)
        run P(P2)
    }
}

では、モデル検査ツールにかけてみましょう。 そうすると、invalid end stateというメッセージが出力されます。 これは、網羅的な検査をしていく中で、実行可能な文がなくなったことを意味します。

spin -a -o2 ./deadlock.pml
gcc -DREACH -o ./pan ./pan.c
./pan -c0 -e
pan:1: invalid end state (at depth 5)
(中略)
spin -k deadlock.pml1.trail -t ./deadlock.pml
spin: trail ends after 6 steps
#processes: 3
        gap_lock_by_P1[0] = 1
        gap_lock_by_P1[1] = 0
        gap_lock_by_P1[2] = 0
        gap_lock_by_P1[3] = 0
        gap_lock_by_P1[4] = 0
        gap_lock_by_P1[5] = 0
        gap_lock_by_P2[0] = 1
        gap_lock_by_P2[1] = 0
        gap_lock_by_P2[2] = 0
        gap_lock_by_P2[3] = 0
        gap_lock_by_P2[4] = 0
        gap_lock_by_P2[5] = 0
  6:    proc  2 (P:1) ./deadlock.pml:53 (state 10)
  6:    proc  1 (P:1) ./deadlock.pml:53 (state 10)
  6:    proc  0 (:init::1) ./deadlock.pml:90 (state 4) <valid end state>
3 processes created

出力された結果を見ると、53行目のinsertの中で処理が先に進めなくなったことがわかります。 2つのプロセスP1、P2がgap0に対しselect for updateで共有ロックをとったものの、お互いinsertに進行ができなくなったようです。 これがMySQLでいうところのdeadlockに相当します。

また、今回トランザクション内の処理(execの中身)は、select for update -> insert -> commitとしていますが、 実際の運用では、このトランザクション内の処理を、プロダクトコード内の処理にあわせてCRUDの組み合わせや順番を変更することを想定しています。 先に書いたとおり、モデル検査には「モデル作成の難易度が高い」というハードルがあります。 それに対しては、例えば、MySQLのlockの機構のモデルをライブラリという形で提供し、query logからexecの中身を自動生成するようなことができれば、 より低コストでモデル検査を行える可能性があると考えています。

設計確認としてのモデル検査

UIのようなイベント駆動での非同期処理の設計確認にもモデル検査が使えます。 少し簡略化して書きますが、APIから取得した要素をリストビューに追加し、追加要素の中心となる位置までスクロールするという処理を実装した際、以下のような設計ミスに気づいた事例がありました。

  • 意図していた挙動
    • 追加するリスト要素を取得
    • 要素を追加表示
    • 中心となる要素の位置を計算
    • その位置までスクロールする
  • 起きてしまった意図しない挙動
    • 追加するリスト要素を取得
    • 中心となる要素の位置を計算
    • その位置までスクロールする(追加表示が完了していないので、実際はスクロールは行われなかった)
    • 要素を追加表示

非同期処理のように考慮漏れが起きやすい処理の設計の検証にもモデル検査が有効です。

/**
 *
 * 想定する仕様:
 * もともと、要素数が5のリストに、4つの要素を追加して、全体として9つの要素にする
 * 9つの要素が表示されたら、7番目の要素の位置までスクロールをする
 *
 */

int item_num = 5 // 初期の要素数
int position = 3 // 初期のスクロール位置

chan position_request = [0] of { bool } // スクロール位置更新リクエスト用の変数
chan item_num_request = [0] of { bool } // リスト更新リクエスト用の変数

chan position_reply = [0] of { bool } // スクロール位置更新リクエストの返信用の変数
chan item_num_reply = [0] of { bool } // リスト更新リクエストの返信用の変数

active proctype Items() {
    int new_item_num = 9
end:
    do
    :: item_num_request ? _ -> // リスト更新のリクエスト受信待ち
        // リスト更新のリクエストを受け取ったら、リストの要素数を9にする
        // 本来のプロダクトなら、"9"を導き出すロジックがあるはずだが、
        // モデル検査においてはそのロジックは関係ないので割愛
        item_num = new_item_num
        item_num_reply ! true
    od
}


active proctype Position() {
    int new_position = 7
end:
    do
    :: position_request ? _ -> // position更新のリクエスト受信待ち
        // スクロール位置更新のリクエストを受け取ったら、スクロール位置を更新する
        // ただし、要求されたスクロール位置が、リストに存在しない場合は更新処理をskipする
        if
        :: item_num >= new_position ->
            position = new_position
        :: else -> skip
        fi
        position_reply ! true
    od
}

proctype ClientA() {
    item_num_request ! true -> item_num_reply ? _ // リスト更新のリクエストを投げ、返信として新しい要素数が返るのを待つ
}

proctype ClientB() {
    position_request ! true -> position_reply ? _ // スクロール位置更新のリクエストを投げ、返信として新しいスクロール位置が返るのを待つ
}

init {
    atomic {
        run ClientA()
        run ClientB()
    }
    // 必ず、「9つの要素が表示されたら、7番目の要素の位置までスクロールをする」が満たされることを検査する
    (_nr_pr <= 3) -> assert(item_num == 9 && position == 7)
}

モデル検査にかけると、反例として、「要素数は9だが、スクロール位置は3」が経路と合わせて出力されます。

spin -pglsr -k rx.pml1.trail -t ./rx.pml
using statement merging
Starting ClientA with pid 3
  1:    proc  2 (:init::1) ./rx.pml:58 (state 1)    [(run ClientA())]
Starting ClientB with pid 4
  2:    proc  2 (:init::1) ./rx.pml:59 (state 2)    [(run ClientB())]
  3:    proc  4 (ClientB:1) ./rx.pml:53 (state 1)   [position_request!1]
  4:    proc  1 (Position:1) ./rx.pml:36 (state 1)  [position_request?_]
  5:    proc  3 (ClientA:1) ./rx.pml:49 (state 1)   [item_num_request!1]
  6:    proc  0 (Items:1) ./rx.pml:22 (state 1) [item_num_request?_]
  7:    proc  1 (Position:1) ./rx.pml:42 (state 4)  [else]
  8:    proc  1 (Position:1) ./rx.pml:42 (state 5)  [(1)]
  9:    proc  1 (Position:1) ./rx.pml:44 (state 8)  [position_reply!1]
 10:    proc  4 (ClientB:1) ./rx.pml:53 (state 2)   [position_reply?_]
 11: proc 4 terminates
 12:    proc  0 (Items:1) ./rx.pml:26 (state 2) [item_num = new_item_num]
 13:    proc  0 (Items:1) ./rx.pml:27 (state 3) [item_num_reply!1]
 14:    proc  3 (ClientA:1) ./rx.pml:49 (state 2)   [item_num_reply?_]
 15: proc 3 terminates
 16:    proc  2 (:init::1) ./rx.pml:62 (state 4)    [((_nr_pr<=3))]
spin: ./rx.pml:62, Error: assertion violated
spin: text of failed assertion: assert(((item_num==9)&&(position==7)))
 17:    proc  2 (:init::1) ./rx.pml:62 (state 5)    [assert(((item_num==9)&&(position==7)))]
spin: trail ends after 17 steps
#processes: 3
        item_num = 9
        position = 3
 17:    proc  2 (:init::1) ./rx.pml:63 (state 6) <valid end state>
 17:    proc  1 (Position:1) ./rx.pml:35 (state 9) <valid end state>
 17:    proc  0 (Items:1) ./rx.pml:21 (state 4) <valid end state>
5 processes created

今回の実装で問題だったのは、別々のストリームで非同期処理をおこなっていたことによるものでした。 ログを見ると、

  1. 中心位置取得のリクエストが投げられる
  2. 追加するリスト要素を取得するリクエストが投げられる
  3. 中心位置取得のリクエストの結果が返ってくる
  4. 追加するリスト要素を取得するリクエストの結果が返ってくる

となっており、意図していた挙動とは異なることがわかります。

別々のストリームで非同期を走らせると、処理順番は常に同じにならないというのは、非同期処理を扱う上で基本的な話ではあります。 しかし、設計の欠陥に実装した後気づくのと、実装の前に気づくのとでは手戻りのインパクトが異なります。 今回の場合、別々のストリームを作るのではなく、単一のストリームで実装すればよいわけですが、 これは、非同期処理実装時の注意の勘所がわかっていないと気づくのは難しいです。 自分が考えている設計が本当にこれでよいのか、考慮漏れはなさそうかを確認するためにも有効だろう思いました。

今後の展望

いくつかのPoC作りを通して、モデリング言語で表現できる範囲がわかってきました。 これからは、実際のプロダクトコードにモデル検査を適用できる部分を探してみたいと思います。 もともと私が感じていた仕様書問題に対するアプローチという動機からは少し変わり、設計・実装に対するアプローチとなっておりますが、 まずはその方向でさらなる可能性を見極めていきたいと思います。

さいごに

このようにSWETでは、少しずつではありますが、形式手法の可能性について研究をしています。 経過をかなり省いて書いてしまった部分もありますが、なにか疑問がございましたら、@hoddy3190までお尋ねくださいませ。

また、builderscon tokyo 2019に「形式手法を使って、発見しにくいバグを一網打尽にしよう」というタイトルで出したCFPが採択されました! 本記事の続報についてはbuildersconでお話できればと思っています。 是非会場にお越しください!

形式手法の研究は私含め2人のメンバーで行っております。 2人とも半年ほど前に形式手法をはじめたばかりです。 興味がある方、詳しい方、一緒に働いてみようと思ってくれた方、 採用へのご応募お待ちしております!

SWET (Software Engineer in Test) / テストエンジニア (ゲームアーキテクチャ)

また、その他のチームへのご応募お待ちしております。 募集職種に関してましては、本ブログサイドバーの「採用情報」の項目をご覧ください!

AndroidX x JUnit5でUIテストを書こう

こんにちは。SWETチームの@zhailujiaです。 今回はAndroidX x Junit5を使ったUIテストの書き方を紹介して行きたいと思います。

背景

  • JUnit 5はJUnit 4と比べて複数な新機能があって使いたいところですが、現時点GoogleはAndroidのJUnit 5テストをまだ公式対応していません。
  • android-junit5は、AndoridプロジェクトでもJUnit5の使用を可能にするサードパーティ製のGradle Pluginです。
  • 今年4月に、android-junit5から新しいInstrumentationサポート用のInstrumentation 1.0.0Libraryをリリースしました。
  • これによりAndroidXのActivityScenarioなどのAPIとJunit5を組み合わせたUIテストが遂に書けるになりました。

システム環境

System Requments

  • Android Gradle Plugin 3.2.0 or higher
  • Gradle 4.7 or higher
  • Java 8
  • Android 8.0/API 26/Oreo or higher

今回動作確認したバージョン

  • 開発環境
    • AndroidStudio 3.4.1
    • Android Gradle Plugin 3.2.1
    • Gradle 4.10.1
    • Java SE 1.8.0_181
  • 検証端末
    • Android 9.0/API 28

android-junit5プラグインの導入

  • 下記のサンプルはInstrumentation Tests on JUnit 5に必要最小限の設定を記述しています。
  • Android Instrumentation Test on JUnit 4、Local Unit Test on JUnit 5も同一プロジェクトに設定して共存できますが、今回は割愛します。

1. プロジェクトルートのbuild.gradle

buildscript {  
    dependencies {
        // 1) Add android-junit5 plugin to project
        classpath "de.mannodermaus.gradle.plugins:android-junit5:1.4.2.0"
    }
}

2. モジュールのbuild.gradle

android {
    defaultConfig {
        // Use AndroidX test runner
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        // 2) Connect JUnit 5 to the runner
        testInstrumentationRunnerArgument "runnerBuilder", "de.mannodermaus.junit5.AndroidJUnit5Builder"
    }  

    // 3) Java 8 is required, add this even if minSdkVersion is 26 or above
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }  

    // 4) JUnit 5 will bundle in files with identical paths; exclude them
    packagingOptions {
        exclude("META-INF/LICENSE*")
    }
}  

// 5) (Optional) If use ParameterizedTest ArgumentsProvider, this is required for set to recompile with "-jvm-target 1.8"
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
    kotlinOptions {
        jvmTarget = "1.8"
    }
}  

dependencies {
    // 6) Jupiter API
    androidTestImplementation "org.junit.jupiter:junit-jupiter-api:5.4.2"   
    // 7) (Optional) Jupiter Parameters
    androidTestImplementation "org.junit.jupiter:junit-jupiter-params:5.4.2"

    // 8) JUnit 5 instrumentation companion libraries
    androidTestImplementation "de.mannodermaus.junit5:android-test-core:1.0.0"
    androidTestRuntimeOnly "de.mannodermaus.junit5:android-test-runner:1.0.0"

    androidTestImplementation "androidx.test.espresso:espresso-core:3.1.1"
}

Instrumentation Testテストを書こう

ProjectソースはAndroid-Junit5/Instrumeny/Sample/から引用しています、ご参照ください。

AndroidX ActivityScenario on JUnit 5

1. ActivityScenarioExtensionから実現できるJUint5 Instrumentationテスト

  • android-junit5はActivityScenarioExtensionを提供することで、ActivityScenario APIを使用可能になりました。
  • ActivityScenarioExtensionはAndroidX x JUnit4のActivityScenarioRuleのJUnit 5版です。
  • Android Test x JUnit4のActivityTestRuleは廃止予定なので、そのJUnit 5版@ActivityTestも廃止になりました。
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import de.mannodermaus.junit5.ActivityScenarioExtension
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension

/* Android Oreo/8.0/API 26 以下はスキップされる */
class ExampleInstrumentedTest {
  
    /*
     Instrumentation Testのために、Activityを起動します
     @JvmField annotation
       - kotlinで書いているなら必要
     @RegisterExtension annotation
       - ここが最重要、ActivityScenarioのExtension
       - JUnit 4の@RuleのJUnit 5版
     ActivityScenarioExtension.launch<activityClass>
       - ActivityScenarioRuleの代わりに使う
       - activityClassを起動します
    */
    @JvmField 
    @RegisterExtension 
    val scenarioExtension = ActivityScenarioExtension.launch<ActivityOne>()

    @Test
    fun testExample() {
        onView(withId(R.id.textView)).check(matches(withText("0")))
    }
}

2. ActivityScenarioExtensionとActivityScenarioの関係

  • ActivityScenarioExtensionからActivityScenarioを取得できる
    @Test
    fun testExampleUseScenario() {
        // use scenarioExtension to get ActivityScenario
        val scenario = scenarioExtension.scenario
        // then get Activity just same as JUnit 4
        scenario.onActivity { activity ->
            assertEquals(0, activity.getClickCount())
        }
    }

3. Use ActivityScenario with "JUnit 5 Parameter Resolution" feature

  • ActivityScenarioExtensionはJUnit5のParameterResolverを継承しているので、Parameter Resolution機能に対応している。
  • そのため、ActivityScenarioは、直接テストケースのパラメータとして使うことができる。
    // A scenario can be passed into a test method directly, when using the ActivityScenarioExtension
    @Test
    fun testExampleWithParameter(scenario: ActivityScenario<ActivityOne>) {
        scenario.onActivity {
            assertEquals(0, it.getClickCount())
        }
    }

JUnit 5をもっと使ってみよう

Android JUnit 4とJUnit 5のAnnotationマッピング

JUnit 4 JUnit 5
@org.junit.Test @org.junit.jupiter.api.Test
@RunWith deprecated
@Ignore @Disabled
n/a @DisplayName
n/a @Nested
n/a @ParameterizedTest + <Source>
n/a @RepeatedTest(int)
n/a @TestInstance
Assert.assertXXX Assertions.assertXXX

これらのうち、JUnit5で特徴的な、次の2つの機能について、具体的な使い方を紹介します

  • テストの構造化に使う@Nested@Display
  • パラメトライズドテストに使う@RepeatedTest@ParameterizedTest

1. NestedとDisplayName

  • すでにJUnit 5のunit testで馴染まれているNestedとDisplayNameアノテーションはinstrumentation testでも使えます。
  • テストの構造化、日本語の表示を実現できます。
    @Nested
    @DisplayName("Test Group")
    inner class NestedTests {
        @Test
        @DisplayName("テストサンプル")
        fun testExample() {
            onView(withId(R.id.textView)).check(matches(withText("0")))
        }
    }

テスト実行結果

f:id:swet-blog:20190605122005p:plain:w300:left

2. RepeatedTest

  • 指定した回数で繰り返しテストを実行できます。
  • RepetitionInfoからcurrentRepetition(現在の実行回数)、totalRepetitions(実行すべき総回数)を取得できます。
  • RepeatedTestはcustom DisplayNameに対応している。
    // repeat this test 3 times
    // combines a custom display name pattern of each repetition via the name attribute
    @RepeatedTest(value = 3, name = "{currentRepetition}/{totalRepetitions} clickCount={currentRepetition}, expected=''{currentRepetition}''")
    fun repeatedTestExample(repetitionInfo: RepetitionInfo) {
        val count = repetitionInfo.currentRepetition

        for (i in 0 until count) {
            onView(withId(R.id.button)).perform(click())
        }
        onView(withId(R.id.textView)).check(matches(withText(count.toString())))
    }

テスト実行結果

f:id:swet-blog:20190605125839p:plain:w480:left

3. ParameterizedTest

  • ParameterizedTestは異なる引数でテストを複数回実行できます。
  • テストデータは引数ソースSources of Argumentsアノテーションで宣言する。
    • ソースアノテーションは複数種類ありますが、今回は展開せず@ArgumentsSourceを使う例を挙げます。
  • テストメソッドのパラメータとしてデータを受け取ります。
  • ParameterizedTestもcustom DisplayNameに対応している(今回は省略)。
    // define enum for show action description on test results
    enum class Action(val rawValue :ViewAction) {
        Click(click()),
        DoubleClick(doubleClick()),
        LongClick(longClick())
    }
    
    // define custom arguments class as an implementation of ArgumentsProvider
    // ArgumentsProvider must be declared as either a top-level class or as a static nested class
    private class ButtonTestArguments : ArgumentsProvider {
        override fun provideArguments(context: ExtensionContext?): Stream<out Arguments> = Stream.of(
            Arguments.arguments(Action.Click, 1),
            Arguments.arguments(Action.DoubleClick, 2),
            Arguments.arguments(Action.LongClick, 1)
        )
    }

    @ParameterizedTest
    // @ArgumentsSource is one of the Sources of Arguments Annotation
    @ArgumentsSource(ButtonTestArguments::class)
    // get parameters(action,expected) from the Sources of Arguments named ButtonTestArguments
    // can use both of parameterized arguments(action, expected) and extensions parameter resolution(scenario) at the same time
    fun parameterizedTestExample(action: Action , expected: Int, scenario: ActivityScenario<ActivityOne>) {
        onView(withId(R.id.button)).perform(action.rawValue)

        scenario.onActivity {
            assertEquals(expected, it.getClickCount())
        }
    }

テスト実行結果

f:id:swet-blog:20190605125018p:plain:w480:left

パラメタライズドテストは以下のような効果が考えられます

  • RepeatedTestとParameterizedTestを活用して、重複するテストコード、テストケースを削減できます。
  • argumentsを利用してにテストのGiven-When-Thenをペアに設定ことで可読性が上がります。
  • Esspressoだけで画面上複数のビューの操作する時、テストコードがダラダラ長くなりがち部分も、arguments化して綺麗にまとまります。

終わりに

  • 今回紹介したJUnit 5を使えるAndroid UIテストの書き方は分かりやすいですか? ご興味があれば試してみてください。
  • android-junit5はまだリリースされたばかりなので、RepeatedTestとParameterizedTestの動作にはいくつか注意点がありますが、今回は割愛させていただきます。
  • 紹介した内容以外にもAndroidX Fragment Test、Android Test OrchestratorやJUnit5 Test Instance Lifecycleなど一歩進んだ使い方もあります。
  • 今回はこれで終わりにしようと思いますので、今度チャンスがあればまた一緒にAndroidX x JUnit5のさらなる活用を使ってみましょう。

参考リスト

謝辞

  • @marcelschnellさんと共同開発者たちが素晴らしいプラグインを開発してくださって深く感謝しています。
  • 同じくSWETグループの外山さん田熊さんからAndroidテストとJUnit 5に関して貴重な意見と経験を教えていただきました。ありがとうございます。

テストの社内普及のための取り組みとして、Androidテストハンズオンを実施しました

こんにちは、SWETグループの田熊です。

先日、社内のAndroidアプリエンジニアを対象にユニットテストのハンズオンを行いました。 本記事では、どのようなことを考えながらハンズオンを作成していったのかと、ハンズオンの内容を紹介しようと思います。

なぜハンズオンを開催したのか

これまでのSWETは、メンバーがプロダクトに個別にジョインし、品質保証や生産性向上につながるような取り組みをしてきました。 一方、SWETが関わっていないプロダクトからも「テストがなくてつらい」などの声があがっていたため、自動テストのナレッジをスケールさせていくことも必要だと考えていました。 そこで、自動テストの普及の一貫としてテストハンズオンの実施を検討しました。

社内状況の把握

DeNAはサービスが多岐に渡るため、SWET内で各サービスにどの程度自動テストが普及しているかを把握できておらず、どのようなレベル感のハンズオンを作成すればいいかがわかりませんでした。 まずは現状をより正確に知るために、社内のエンジニアに自動テスト実施状況を確認するアンケートを取りました。

その結果、下記のようなことがわかりました。

  • ユニットテストについて
    • 約50%がもっと力を入れていきたいと考えている
    • 約25%が導入したいと思っているが、できていない
    • 作成における課題として多かったもの
      • テストが書きにくい設計になっている(50%)
      • 工数がとれない(40.9%)
      • 費用対効果がわからない(13.6%)
  • UIテストやE2Eについて
    • 約40%が導入したいと思っているが、できていない
    • 作成における課題として多かったもの
      • 工数がとれない(36.4%)
      • ナレッジがない(36.4%)
      • テストが書きにくい設計になっている(31.8%)

この結果をうけて、まずはユニットテストにおける課題の半数を占める「テストが書きにくい設計になっている」について、ハンズオンを通じて何かしらのアプローチができないかと考えました。

なお、初回のターゲットを検討した結果、ネイティブAndroidアプリエンジニアを対象に行うことにしました。理由としては、Androidテスト全書など、ハンズオンの参考になるドキュメントが充実していたからです。

ハンズオンの構成

「テストが書きにくい設計になっている」問題に対してのアプローチを、プロダクトのコードを変更する目的ごとに考えました。

変更の目的 テストが書きにくい設計問題へのアプローチ
新しい機能を追加する 実装と同時にテストを書くことでテストを書きやすい設計にする
既存の機能を改修する 少しづつテストを導入しつつテストが書きにくい設計を改善する

そして、各アプローチを盛り込んだ「テストをはじめよう編」と「テストのないアプリにテストを書こう編」の2つのテーマでハンズオンを作成をすることにしました。 また、各人の時間の都合やパフォーマンス等を鑑み、ハンズオンは2日にわけてそれぞれ2時間で実施とすることにしました。

テストをはじめよう編

テストを始めよう編のアウトラインは下記です。

  • 座学
    • なぜテストを書くのか
    • Androidにおけるテストの説明
    • TDDの紹介
  • チュートリアル
    • TDDチュートリアル(参加者に実際に手をうごかしてもらう)
  • 自習(各自お題をTDDで進めてもらう)

アウトラインについて、どうしてこのような構成にしたかを振り返ります。

なぜテストを書くのか

この章では、テストを書くことによるメリットと、書かない場合のリスクについてまとめています。Androidテスト全書の1章とSWET内部で作成した自動テストの効能をまとめたドキュメントを参考にしながら作成しました。

テストを書く習慣をつけるためには、そのメリットについて納得している必要があります。 自動テストのメリットについて昨今のエンジニアであれば一度は聞いたことがあるかもしれません。しかし、普段テストが書きにくいプロダクト開発をしている場合、日常的に自動テストに触れられておらず、そのメリットの認識も曖昧になっているかもしれないと思いました。

よって、なぜテストを書くのか?を改めて確認し、テストを書くモチベーションの向上につなげようという意図があり冒頭に説明をいれました。

Androidにおけるテストの説明

Androidにおけるテストの分類(Unit test、Integration test、UI test)をまとめています。Androidテスト全書の1章を参考にしながら作成しました。

この章はAndroidアプリ開発におけるテストの基礎知識を整理するために追加しました。

TDDの紹介/TDDチュートリアル

TDDは下記の点から題材として最適と思い採用しました。

  • テストを書きつつテストが書きにくい設計になるのを防ぐために、TDDのアプローチが有効
  • 問題を小さく切り分ける・不安をテストにするといったTDDのエッセンスは、TDDでなくともテストを書いていく上で重要
  • 上記のことを学びつつ、テストの書き方を学ぶこともできて一石二鳥

資料

実際にハンズオンで使用した資料をアップしましたので、ご興味がある方はどうぞ!
※外部公開に際し、資料は一部修正を加えております。

Androidテストハンズオン基礎編

TDDチュートリアル

テストのないアプリにテストを書こう

テストのないアプリにテストを書こう編のアウトラインは下記です。

  • レガシーコードを改善しよう
  • レガシーコード改善に役立つ知識
    • テストダブル
    • レガシーコード改善テクニック
  • テストのないアプリにテストを書こう(参加者に実際に手をうごかしてもらう)

こちらもどうしてこのような構成にしたかを振り返ります。

レガシーコードを改善しよう

この章では、自動テスト実施状況アンケート結果で社内の半数がテストが書きにくい設計を課題としていることを紹介し、地道にテストを導入しつつテストが書きにくい設計を改善しようという提案をしました。

目的は、タイトルの通りレガシーコード改善への動機づけです。テストが書きにくい部分に引きずられてテストを書かずにいると、悪循環になってしまうからです。

レガシーコード改善に役立つ知識

ここでは、テストダブルとレガシーコード改善テクニックの紹介をしました。Androidテスト全書の2章とレガシーコード改善ガイドを参考に作成しました。

テストが書きにくい設計のアプリにテストを書いていくのは、なかなかに難易度が高いタスクです。 テストダブルを使って依存を切り離すテクニックと、レガシーコード改善ガイドにある最低限の修正でテストを書けるようにするテクニックを知っていると、そのタスクの助けになると思いこの章を追加しました。

テストのないアプリにテストを書こう

あえてテストが書きにくい設計で実装したサンプルアプリを題材に、参加者にユニットテストを書いてもらうセクションです。 成果物はPull Requestとして提出してもらい、SWETメンバーがレビューを行う方式にしました。

ここでは、その前の章で説明したテストダブルやレガシーコード改善テクニックを、参加者が実際に使って身につけてもらうことを目的としています。 また、Pull Requestレビューを通じて説明した内容がきちんと伝わっているかと、参加者がお題にどのように取り組んだかを確認できるようにしました。

資料

こちらも実際にハンズオンで使用した資料の一部をアップしましたので、ご興味がある方はどうぞ!

※外部公開に際し、資料は一部修正を加えております。

テストのないアプリにテストを書こう編

ハンズオンの振り返りとその先

ハンズオン実施後に参加者アンケートを取った結果、各ハンズオンの総評は「テストをはじめよう編」が平均4.88、「テストのないアプリにテストを書こう編」 が平均4.78と良いフィードバックをいただくことができました。(5段階評価で5が良い・1が悪い)

ハンズオンの内容の質にこだわったのはもちろん、専用のslackチャンネルを設けて質問をしやすくしたり、講師以外にもTAを3人配置するなどといった運営の工夫も功を奏したのだと思います。

ハンズオンは好評のうちに終えることができましたが、実際にプロダクト状況を改善していくには継続的な取り組みが必要になります。 参加者のテスト導入について相談を受けられるようにし、実プロダクトへの貢献も進めていきたいと考えています。

また、今後もSWETでは社内のエンジニアがテスト技術について学ぶことができるコンテンツの作成を進めていきます。

現在、下記を企画しています。

  • ユニットテストハンズオンのiOS編
  • モバイルのUIテストハンズオン
  • Go言語でTDDを学ぶことができるCodelab

このような取り組みを通じて、社内のエンジニアが自動テストスキルを向上できる仕組みと、社内のエンジニアがいつでもテストについてSWETに相談できるような環境を作っていきます。

最後に、SWETでは一緒に自動テストの普及に取り組んでくれるエンジニアを募集しています。 今回の取り組みに興味を持たれた方は、下記URLからのご連絡をお待ちしております!

募集職種: SWET (Software Engineer in Test) / テストエンジニア (Mobile)

DroidKaigi 2019にSWETから3名が登壇してきました

こんにちは。SWETチームです。
2/7(木)、2/8(金)に開催されたDroidKaigi 2019にSWETからは3名が登壇しました。
昨年よりもさらに盛り上がりを見せていて、どのセッションもどのブース展示も非常に有意義で面白かったですよね!

今回は、

  • 登壇者より自身のセッションについて一言
  • 参加メンバーが特に印象に残ったセッション

をご紹介していきたいと思います!

f:id:swet-blog:20190215164001j:plain

登壇者より自身のセッションについて一言

マルチモジュールなプロジェクトでテストはどう変わる?

登壇者の田熊(@fgfgtkm)です。

このセッションでは私がマルチモジュールなプロジェクトでのテストを考えたときに、変えていく必要があると思った3点を紹介させていただきました。

  • DI
  • テスト方針
  • メトリクスの収集

DIのセクションでは、循環参照の問題や依存解決の定義をモジュール内に閉じ込めるため、これまでのやり方を変更しなければいけない可能性があることを話しました。 話しきれなかった部分についての詳細は『マルチモジュールプロジェクトでのDagger2を用いたDependency Injection』のセッションを紹介させていただきました。私も拝見させていただき、とてもわかりやすく勉強になりました。

テスト方針のセクションでは、モジュール単位で動作確認ができるようにテスト方針を考える、という話をしました。 ここではProject Nitrogenについて触れ、自動テストでカバーする領域を増やしていける環境が整いつつあることもご紹介させていただきました。

最後にメトリクス収集について、複数のモジュールのレポートをまとめて見たい場合マージが必要になることと、PITというツールを例にどのようにレポートをマージするかについて話しました。

想定していたよりも大きな部屋で多くの開発者に話をさせていただけることになったので、マルチモジュールというトレンドを取り入れつつも、これからテストにもっと力を入れていきたいと思っている方に下記のことを伝えられたらいいなと思いながら構成を考えました。

  • 自分のプロジェクトにどんなテストが必要かを考える
  • テストの環境や技術は日々進化している

テストに対する考えや知識をアップデートするきっかけになればとても幸いです。

EspressoのテストをAndroidの最新トレンドに対応させよう

登壇者の外山(@sumio_tym)です。

※動画はこちら

AndroidのUIテストツールEspressoで書かれたテストコードを「最新トレンドに対応させる」と銘打って、 次のようなトピックを解説しました。

もちろん、テストコードのメンテナンス性を維持できるように、Page Objectsデザインパターンを適用する前提で解説しています。 特に、Espresso Test Recorderで生成したKotlinのコードをPage Objectsに書き換える様子をライブコーディングしている点が見どころです。 気になる方は是非動画もチェックしてみてください。

UIテスト(Espresso)の高速化をさらにすすめる

登壇者の平田(@tarappo)です。

DroidKaigi 2018の続編という形で「UIテストの高速化をさらにすすめる」というタイトルで登壇しました。

UIテストの課題になりやすいものの1つに実行時間の増加があります。 このUIテストの実行時間をどのように短くするかという話をしました。

前回話した内容は実際に試してみるのがやや大変だったかと思いますが、今年の登壇内容は誰もがすぐに試せるようになっています。 この登壇資料や動画を見て、実際にFirebase Test LabやFlankを試してみてもらえるとうれしいです。

※動画はこちら

登壇の際に、その先にある課題として自動テストがあることにより得られるred/green以外の情報などについても話しました。 その課題については、どこかの機会で話せると嬉しいなと思っています。

参加メンバーが特に印象に残ったセッション

Spek2+MockK+JaCoCoでイケてるUnit Test環境を手に入れろ!

改めまして田熊(@fgfgtkm)です。

今回はSpek関連のセッションが2つありましたので、それぞれご紹介させていただきます。

1つめは、Subroh Nishikoriさんのセッションです。

こちらのセッションでは、SpekだけでなくKotlin製モックライブラリのMockKやコードカバレッジライブラリのJacocoなどトータルのユニットテスト環境について紹介されていました。

※動画はこちら

Spek2とMockKはどちらもKotlinフレンドリーな記述が可能で、Kotlinに慣れ親しんだ開発者がユニットテストを書き始めるときにとても導入しやすいと思っています。

また、私のセッションでマルチモジュールなプロジェクトの場合レポートのマージが必要なことに触れましたが、こちらのセッションではJacocoでの具体的なマージ方法を紹介していました。

それぞれの導入手順や使い方について丁寧に説明しているため、これからテスト環境を構築していく際の参考にすると良いと思いました。

SpekでUnitTestを書こう

2つめはAyako Morimotoさんのセッションです。

こちらのセッションではSpekの紹介と、Spekで書くのが難しいAndroid Architecture Componentsを使用したクラスのテストをどのようにSpekで書くかを紹介していました。

※動画はこちら

Spekはテストコードの読みやすさから人気が高まっていると感じていますが、Androidが絡むとJUnitに比べて書き方のナレッジやサンプルが少ない印象です。

セッションの中ではAndroid開発の中で採用されることが多いLiveDataとRoomのテストをSpekで書くための工夫が紹介されており、Spekで書けるテストの範囲を増やしていく際に役立つ知見の詰まったセッションだと思いました。

build.gradle.ktsに移行しよう

改めまして外山(@sumio_tym)です。

こちらはtnjさんによる発表です。

※動画はこちら

GradleのビルドスクリプトをKotlin DSLへ移行する方法についての発表でした。 静的型付き言語であるKotlin DSLへ移行することで、IDEによるコード補完が効きやすくなるというメリットがあります。

Gradleのビルドスクリプトでは、GroovyとKotlinを共存させることができます。 その特徴を最大限利用して、徐々にGroovyからKotlinへ置き換えていくテクニックを分かりやすく解説していました。

実は、外山自身も以前にKotlin DSL化しようとして挫折した苦い経験があります。 その当時を思い返してみると、着手する場所が悪かったがために、芋蔓式に全部Kotlin化せざるを得なくなったのが敗因でした。

この発表で解説されている順番にしたがえば徐々に書き換えられるので、挫折せずにKotlin化を完遂できそうです。 Kotlin DSLへの移行を検討されている方は、是非参考にしてみてください。

Google Play Consoleのリリーストラックを有効活用してリリースフローの最適化を行った話

SWET CI/CDチームの加瀬(@Kesin11)です。

こちらのlitmonさんのセッションは、CIサービスやデプロイの自動化などに興味があるCI/CDチームとして、参加前から注目していました。

※動画はこちら

発表内容のメインは、それなりの人数で開発されているであろうアプリのリリース作業について人間が調整するのにコストが高くなりすぎたので、自動的に毎週リリースされるフローを構築したというものでした。
その中でも個人的に最も興味を持ったのはGoogle Play Consoleのリリース前レポートでした。

発表では、このリリース前レポートを単純なクラッシュ検知として使うことは残念ながら諦めたという話でしたが、発表後に直接お話を伺ったところFirebase Test LabのRoboテストを直接利用することで同等のクラッシュ検知と通知は実現しているとのことでした。ドキュメントによるとPlay Consoleで使用されているのはこのRoboテストのようです。

個人的な感想ですが、リリース作業を自動化するというフローはそもそもプロダクト全体の品質がかなり高い状態でないと、逆に検証不足でバグが頻出してしまうことも起こり得るのではないかなと思います。
クックパッドでは以前から大規模にUIテストを含めた自動テストが行われているということをブログなどで拝見していたので、そのような積み重ねがあってこそ達成できるリリースフローなのかもしれません。この発表ではテストについての話はありませんでしたが、テストの必要性をより意識した発表でした。

ああ、素晴らしきTDD ~ アプリとエンジニアの心に安寧を ~

SWET AutomationTestチームの鈴木です。
私の方からは、Saiki Iijimaさんの発表をご紹介いたします。

※動画はこちら

本セッションでは、登壇者自身の実際の開発経験を踏まえながら、TDDによって得られるメリットについて解説されていました。
ライブコーディングでTDDを実演しながら説明されていたので、初心者の方でも大きな抵抗なく読み進めることができると思います。
内容はもちろん、説明の仕方もわかりやすく大変参考になりました。

おわりに

DroidKaigi 2019のセッションのうち、登壇者のセッションと各メンバーがそれぞれ印象に残ったセッションをご紹介しました。

SWETグループでは、DroidKaigiでの登壇に限らず、積極的に対外的なアウトプットを行っています。
加えて、SWETグループ主催の勉強会である、Test Night(テストについて語り合う勉強会)も定期的に開催しております。
直近では、CI/CD Test NightAndroid Test NightやiOS Test Night(4月開催予定)の開催を予定しております。
ご興味のある方は是非ご参加ください。

また、SWETグループではAndroidに限らずテストの自動化やCI/CDに興味があり、私達と一緒に働いてくれる仲間を募集しています。

ご応募お待ちしています!

2018年の暮れにXcode 10のテスト周りの機能をふりかえる

この記事はDeNA Advent Calendar 2018 その2の最終日の記事です

SWETの平田(@tarappo)です。 アドベントカレンダーも本日で最終日です。 そして、2018年は残りわずかとなりました。

そこで今回は今年おこなわれた「WWDC 2018」の「What's New in Testing」をふりかえってみたいと思います。

「WWDC 2018」の動画やスライドは公開されており、以下から見ることができます。 今だと日本語訳もありますし、見て参考になるものが多いかと思います。

この中から「What's New in Testing」は以下になります。

さて、この「What's New in Testing」で紹介されたのは以下の3種類になります。

  • Code coverage
  • Test selection and ordering
  • Parallel testing

Xcode 10から追加された機能もあれば、ベースの機能があってそれが改良されたものもあります。

今回は、これらの機能の中から主に「Test selection and ordering」と「Parallel testing」について紹介したいと思います。

まだそこまで使ってみた情報が世に転がっていないので、是非とも使ってみて様々な知見を世に公開してもらえると嬉しいです。

Code coverage

Xcode 9.3で改良がおこなわれて動作や精度が向上しています。 実際に発表された例では以下のように改善されています。

  • Xcodeでロードして表示されるまでの時間
    • Xcode 9:6.5 sec
    • Xcode 9.3:0.5 sec
  • カバレッジファイル
    • Xcode 9:214 MB
    • Xcode 9.3:18.5 MB

他にもありますが、今回のメインは残り2つですのでこれぐらいにしておきます。 Code coverageについての話は別途どこかしらで紹介できればと思います。

Test selection and ordering

ここで紹介する「Test selection and ordering」と次に紹介する「Parallel testing」で紹介される機能については、Xcode 10以降のスキームエディタから設定できるようになっています。

その設定場所はXcodeの「Edit Scheme」を選択した後、「Test」を選ぶと表示される以下の画面にあります。

f:id:swet-blog:20181225115928p:plain
Xcode10のスキームエディタ

この画面の「Options」を選択すると表示される以下の3項目が今回紹介する機能になります。

f:id:swet-blog:20181225115946p:plain
Xcode10のオプション

この「Test selection and ordering」では以下の2つの項目について紹介します。

  • Automatically include new tests
  • Randomize execution order

そして次の「Parallel testing」では以下の1つの項目について紹介をします。

  • Execute parallel on Simulator

Automatically include new tests

この機能は「新しく作成したテストコードを自動的に実行対象に含めるかどうか」の設定になります。 このオプションを無効にした場合、新規で実装したテストコードは常に実行対象外(disable)になります。 デフォルトはチェックがついた有効の状態になっています。

今までは、テストコードの中からあるテストクラスやテストケースを実行対象外にするということができました。 しかし、これだと新規に追加したテストコードは実行対象になってしまいます。

今回のオプションを有効活用をすれば、新規に実装したテストコードは常に実行対象外にするといったことができます。

今までは「実行対象のテストコードを実行対象外にする」という行為のみでしたが、「実行対象外のテストコードを実行対象にする」という逆のやり方もできるようになったということです。

実際の利用ケースを考えてみましょう。 以下の2つのスキームがあるとします。

  • スキームA:定期的に動かすことを想定
    • 本オプションを無効にする
  • スキームB:nightly(夜間)で動かすことを想定
    • 本オプションを有効にする

実装したテストコードは常にスキームBでは実行対象となりますが、スキームAでは実行対象にはなりません。

nightlyで問題なく通りつづけ安定性が確認されたらスキームAのほうで、そのテストコードを実行対象とします。

このようなケースでの使い方もできるようになっています。

このケースはあくまでも例であり、利用方法はいろいろと考えられます。

テストコードを実行対象外にする方法

さて、今回の話からは少し脱線した蛇足的な話になります。 これは開発時において、テストコードを一時的に無視したいときの話です。

Xcodeでテストコードを実装している場合に、あるテストコードを実行対象外にするにはどうしたら良いのでしょうか?

まず、他のテスティングフレームワークはどうなっているのでしょうか? 例えば、JUnit4では@Ignoreアノテーション(JUnit5だと@Disabled)があります。 このアノテーションにはコメントを付与することもできます。

@Ignore("not ready yet")

RSpecだとskipというものがあり、xit というようにxというprefixをつけることでテストをスキップさせることができます。 またpendingというのもあります。

Xcodeの場合だと以下のようにすることで実行対象外にできます。

  • testというprefixを消してしまう
    • XCTestではtestというprefixがついているものをテスト対象として判断されます
  • Xcodeから対象のクラスまたはケースをdisableにする

Randomize execution order

この機能は「対象としたスキームに含まれるテストコードに対して、テストの実行をランダムでおこなうかどうか」の設定になります。

デフォルトではチェックが付いていない無効状態になっています。 無効の状態だとテストの実行順はアルファベット順になっています。

自動テストをランダムに実行できるメリットはなんでしょうか? 自動テストはそれぞれのテストケースが独立していることが望ましいです。

あるテストケースが他のテストケースに副作用を与える形になっていると、次のような問題が起こる可能性があります。

  • テストケースが増えて、実行順序が変わったことによりテストが落ちるようになる
  • 特定のテストだけを動かそうとして動かない

つまり、「全てのテストコードを動かせば成功するが、それ以外の方法で動かすと失敗する」という形になったりします。 しかし、副作用を与えているかどうかということに気づかないこともあります。

そのため、ランダムで実行させるとテストケースが独立していないことに気づく一助となったりします。

さて、このような重要な機能ですが、他のテスティングフレームワークではランダム実行についてはどうなっているのでしょうか?

JUnit4はAndroidでも利用されていますが、このバージョンではランダム実行はできません。

しかし、JUnit5からランダム実行ができるようになっています。 また、ランダム実行をしたのと同じ順番で再度実行をするためのseedというものが発行されるようになっています。

それ以外にもアルファベット順とOrderアノテーション順というのがあります。 詳しくは以下のサイトを確認してみてください。

RSpecでは2.8(2012/01)の時点で、すでにランダムで実行できるようになっています。 そして、JUnit5と同様にseedを発行できます。

再度同じ順番で実行ができるというのは、テストの実行をランダムでおこなう場合は必須の機能とも言えます。

この必須ともいえる機能はどのようなときに必要なのでしょう? テストコードをランダムで実行して失敗し、修正をしたあとに直っているかどうかの確認をするためには同じ順番で実行できることが重要です。

しかし、Xcode 10ではランダムでの実行がサポートされましたが、同じ順番で再度実行できないといった状況です。 つまり再現確認をおこなうことが出来ません。

どのような順番で実行されたかは分かるので、手動で頑張るという荒業はあるにはありますが、なかなか厳しいです。 せっかくの機能ではありますが、これについては致命的です。

今後改善されることを期待したいと思います。

なお、「Open Radar」にはそのことについて書かれたチケットがすでにあります。

Parallel testing

この話は「WWDC 2018」では多少多めに時間をとって説明をしていました。

Xcode 9の時点でxcodebuildコマンドからシミュレーターの並列起動ができるようになり、fastlaneのsnapshotでは対応されました。 しかし、以下の機能不足がありました。

  • xcodebuildコマンドのみの対応
  • destinationはそれぞれ別である必要があった
    • 同じタイプのiPhone Simulatorを選択できませんでした

Xcode 10からはこの不足部分が解消されています。 つまり、Xcodeからでも同じiPhone Simulatorに対して並列でテストを実行できるようになりました。

これはXcodeを使って開発している際においても、テストの実行時間を削減できるようになったということです。

Execute parallel on Simulator

この機能は「対象としたスキームに含まれるテストコードに対して、同じiOSのシミュレーターで並列実行させるかどうか」の設定になります。

デフォルトでこの設定は無効になっていますが、有効にすると1つの親からクローンのSimulatorを作り、並列にテストを実行してくれます。

xcodebuildコマンドのオプションとしては以下が追加されています。 xcodebuild -helpをすると他にも増えていることがわかります。

  • -parallel-testing-enabled YES|NO
    • 並列実行をするかどうか(Xcodeの設定を上書きします)
  • -parallel-testing-worker-count NUMBER
    • 並列数は実行する環境のスペックに依存して決めるのが良いです

この並列時の実行は、Androidで利用されるEspressoのtest shardingとは異なり、テストクラス単位で勝手にわりふられます。

並列化によりテストの実行時間は短縮されます。 ただし、この並列化をおこなうことにおいてよくあるケースとして、1つの実行時間が長いクラスがあるとその実行時間にひっぱられる形となるということです。

例えば、以下の図のようにクラスAからEまで5クラスあったとします。 クラスAの実行時間が長いと、並列数を増やしても最終的にAの実行時間がトータルの実行時間になってしまいます。

f:id:swet-blog:20181225115848p:plain
クラスの分割前

このような状況のときはどうしたら良いのでしょうか? よくいわれる対処法は、実行時間が長いクラスを分割することです。

今回の例ですと、クラスAをクラスA1とクラスA2なりに分割するということです。

f:id:swet-blog:20181225115809p:plain
クラスの分割

これにより、トータルの実行時間は短くなります。

「WWDC 2018」では、必要になったら実行時間が長いクラスを分割しましょうという話がありましたが、必要なタイミングで分割しやすい形になっているとは限りません。

そのため、テストコードを最初から分解しやすい形にしておくことが求められます。

つまり、テストコードでは、「前処理」「実行箇所」「検証」「後処理」といったものをちゃんと分けて実装しておくことが大事です。 XCTestにおいてもsetUp()tearDown()があるので、十分に活用することが大事です。

そして、「Four-phase test」や「AAA」といったものを知って実践するのが良いでしょう。

テストのランダム実行でも同様ですが、より自動テストを適切に実装する必要性が問われるようになってきているとも言えます。

なお、fastlaneでも利用されているテスト結果をファイル形式などで出力してくれるxcprettyはこの並列実行への対応がおこなわれていません。 そのため、fastlaneのscanなどを利用している場合は、テスト結果の出力は不完全な状態ではあります。

並列実行のその次のステップへ

今回のような並列実行ができるようになったというのは次のステップがいろいろと考えられるようになったとも言えます。

「WWDC 2016」で発表されましたが、xcodebuildコマンドでクラスを指定してテストコードを実行するオプション(only-testing)が追加されています。

このようなオプションと組み合わせることにより、さらなるテストの実行時間の最適化はできるようになります。 しかし、現状の仕組みを用いて自身でこの最適化をおこなうのは非常に高コストではあります。

例えば、並列実行に関してはCircleCIではparallelismというkeyを設定することにより並列実行をしてくれるというものがあります。 詳細については以下のページをチェックしてもらえればと思います。

また、Firebase Test Labというデバイスファームを用いて、テストを並列化するFlankというものもあります。

このFlankがBitriseから利用できるようになるという話もあります。

このように、今後はCIサービス側とその先のサービスで並列化をよりサポートをしてくれるようになってくるかもしれません。

おわりに

今回紹介したようにXcode 10からは、xcodebuildコマンドからの実行だけでなくXcodeからの実行においても非常にいろいろと便利になってきています。

自動テストにおける土台は少しずつではありますが進化してきています。

ここ数年の進化を駆使すればいろいろとできることは増えています。 その結果として、いろいろなサービスも生まれてきています。

しかし、まだまだその手の知見が共有できていない状況といえます。 ぜひとも、本記事などを参考に触っていただき得られた知見を世に公開してもらえると嬉しい限りです。

ゼロ円から始めるクラウドの実機を使った自動テスト(iOS)

この記事はDeNA Advent Calendar 2018 その1の2日目の記事です

こんにちは。SWETの加瀬(@Kesin11)です。

2018年も残すところ一ヶ月弱となりましたが、今年一番のニュースと言えば何だったでしょうか? そう、Google IO 2018で発表されたFirebase Test LabのiOS版ですね!

Firebase Test LabはiOSとAndroidに対応した実際の実機デバイス上でテストを実行してくれるサービスです1
Android版は結構前から既に提供されており、Android Studioにも統合されているので使ったことがあるという方も結構いるのではないでしょうか?

一方でiOSに関してもFirebase Test Labと同様の実機デバイス上でテストを実行してくれるサービスはいくつかありますが、Firebase Test Labが将来的にiOSをサポートするかどうかという話はこれまで何もありませんでした。

そのような状況でしたので、Google IO 2018でFirebase Test LabのiOS版が発表されたとき個人的には大興奮でした。そして先日開催されたFirebase Summitでついに、βが取れて正式にリリースされたというアナウンスがありました🎉

iOS版ではテスト用のビルドをアップロードすると、Firebase Test LabにあるiOS実機でXCTest、XCUITestを実行してくれて、デバイスごとのテスト結果や動画などを確認できます。

前置きが長くなりましたが、今回はそんな待望のFirebase Test LabのiOS版について、以下の複数の利用方法についてそれぞれ解説していきます。

  • Bitriseから使う
  • gcloudコマンドから使う(ローカル)
  • gcloudコマンドから使う(CI環境)
  • Fastlaneプラグインから使う

なお、本記事の動作確認はこちらのリポジトリをベースにXCTestを追加し、XCUITestを少し修正したものを使用しています。
使用したXcodeのバージョンは10.1です。
https://github.com/browserstack/xcuitest-sample-browserstack

Bitriseから使う

Bitriseはモバイルアプリに特化したCI/CDサービスです。
もし既にBitriseをお使いであれば、現状ではこの方法が最も簡単、かつ無料の方法になります。

BitriseにはiOS Device Testingというステップが存在します。
これは名前の通りに実機デバイスでXCTest、XCUITestを実行し、テスト結果だけではなくてそのときのスクリーンショット、動画、デバイスログも保存してくれるものです。実は、その裏側はFirebase Test Labを使用していることが公式に解説されています。

Firebase Test Labは本来アカウントの認証が必要であったり、一定以上の使用には課金が必要です(認証と料金については後述)。 しかしiOS Device Testingを使用する場合にはFirebaseとのやりとりをBitriseがやってくれるため、アカウントの認証不要、さらに現在はβ版のためか無料で使うことができます。

使用可能なデバイスの一覧はiOS Device Testingのステップから確認できます。執筆時点での一覧はこのようになっていました。

f:id:swet-blog:20181130104530p:plain
Bitrise iOSデバイス一覧

さすがに最新のiPhone XSなどはないものの、iOS 12のiPhone Xは存在しています。 古めのデバイスも結構揃っており、最も古いものでiPhone 6が存在し、多少ですがiPadも種類が用意されています。さらにiOSのバージョンはiOS 9.0からそろっています。
さすがに全デバイス × パッチバージョンまで含めた全iOSバージョンの全てをカバーしているわけではありませんが、これだけのデバイス × iOSバージョンの実機を実際に手元で用意、管理するのはなかなか大変だと思います。これこそが、この手のサービスで最も嬉しいポイントではないでしょうか。

iOS Device Testingのさらに詳しい使い方やスクリーンショットは、Bitrise公式のドキュメントやブログが充実していますので、そちらを見てもらうと分かりやすいと思います。

Device testing for iOS
Introducing solid and snappy real device testing for iOS with Firebase (beta)

gcloudコマンドから使う(ローカル)

Bitriseを使っていない場合、gcloudコマンドから直接Firebase Test Labを使う方法になります。

https://firebase.google.com/docs/test-lab/ios/command-line?hl=en

Firebase Test Labを使うにはアカウントの認証と、紐付けるプロジェクトの選択が必要になります。
まずはドキュメントに従ってgcloud auth logingcloud config set project PROJECT_IDを実行してください。PROJECT_IDはfirebaseのプロジェクトIDです。もしまだfirebaseのプロジェクトを作成していない場合、先にfirebaseのプロジェクトを作成しておいてください2

gcloudコマンドのセットアップができたら、まずは使用できるデバイスの一覧を見てみましょう。

$ gcloud firebase test ios models list
┌──────────────┬───────────────────────┬─────────────────────┬─────────┐
│   MODEL_ID   │          NAME         │    OS_VERSION_IDS   │   TAGS  │
├──────────────┼───────────────────────┼─────────────────────┼─────────┤
│ ipad5        │ iPad (5th generation) │ 11.2,12.0           │         │
│ ipadmini4    │ iPad mini 4           │ 11.2,12.0           │         │
│ ipadpro_105  │ iPad Pro (10.5-inch)  │ 11.2                │         │
│ iphone6      │ iPhone 6              │ 11.4                │         │
│ iphone6s     │ iPhone 6s             │ 10.3,11.2,11.4,12.0 │         │
│ iphone6splus │ iPhone 6s Plus        │ 9.0,9.1             │         │
│ iphone7      │ iPhone 7              │ 11.2,11.4,12.0      │         │
│ iphone7plus  │ iPhone 7 Plus         │ 11.2,11.4,12.0      │         │
│ iphone8      │ iPhone 8              │ 11.2,11.4,12.0      │ default │
│ iphone8plus  │ iPhone 8 Plus         │ 11.2,11.4,12.0      │         │
│ iphonese     │ iPhone SE             │ 11.2,11.4,12.0      │         │
│ iphonex      │ iPhone X              │ 11.2,11.4,12.0      │         │
└──────────────┴───────────────────────┴─────────────────────┴─────────┘

先述のBitriseと同じリストが表示されましたね。では次にテスト用のビルドを作成します。

ドキュメントにあるようにxcodebuildでビルドします。 アプリ名とスキームがMyAppで、ビルド結果の出力先をfirebase_testとした場合には以下のようなコマンドになります。

$ xcodebuild -project MyApp.xcodeproj \
  -scheme MyApp \
  -derivedDataPath firebase_test \
  -sdk iphoneos build-for-testing

さらにFirebase Test Labでテストを実行するには、テスト用のビルド成果物とxctestrunファイルを1つのzipに圧縮する必要があります。

$ cd firebase_test/Build/Products
$ zip -r MyTests.zip Debug-iphoneos MyApp_iphoneos12.1-arm64e.xctestrun

あるいはFastlaneを導入済みならばscanbuild_for_testingオプションを使ってテスト用ビルドを生成しても大丈夫です。

scan(
  scheme: 'MyApp',
  clean: true,
  skip_detect_devices: true,
  build_for_testing: true,
  sdk: 'iphoneos',
  should_zip_build_products: true
)

テスト用のビルドができたらいよいよテストを実行してみましょう。

$ gcloud firebase test ios run --test MyTests.zip \
  --device model=iphonex,version=12.0,locale=ja_JP \
  --device model=iphone8,version=11.4 \
  --device model=iphone6splus,version=9.1 \
  --device model=iphone6s,version=10.3,locale=ja_JP,orientation=landscape \
  --device model=ipad5,version=12.0 \
  --xcode-version=10.1

テストを実行したいデバイスの設定を--deviceの引数に渡します。modelとversionには先ほどのモデル一覧で表示されたMODEL_IDとOS_VERSION_IDSを指定します。必要であればlocaleとorientationも指定可能で、指定しなかった場合のデフォルトはenとportraitとなります。
指定可能なlocaleについてはgcloud firebase test ios locales listで一覧の確認が可能です。

--xcode_versionはFirebase Test Lab側でテストを実行するXcodeのバージョンのようです。執筆時点ではXcode 10.1でビルドした場合に--xcode_versionの指定無しでも動くことを確認しましたが、古いXcodeを使用している場合や、新しいXcodeが出た直後などにはバージョンを指定しておいた方がいいかもしれません。

特にエラーなくコマンドが実行できたら、まだテスト実行中の段階からFirebase Test Labのページで結果を見ることができます。
実行が完了するとこのように複数のデバイスでテストケースごとのテスト結果、スクリーンショットや動画を確認できます。

f:id:swet-blog:20181130104903p:plain
デバイス毎の実行結果
f:id:swet-blog:20181130104911p:plain
テストケースの結果

gcloudコマンドから使う(CI環境)

ローカルの環境から実行できることは確認できましたが、アプリのビルドやテストはローカルとは別のCI環境で実行していると思いますので、継続的にテストを実行させるにはCI環境からFirebase Test Labを実行をできるようにしておくのがよいでしょう。

しかし、先述のgcloud firebase test iosを実行するにはアカウントの認証が必要です。そして認証にはブラウザでログインする必要があるので通常の方法ではCI環境で認証することはできず、テストを実行できません。

この問題はサービスアカウントを使って認証することで解決できます。この方法に関してのドキュメントはAndroid版の方に一応存在するのですが、あまり分かりやすいとは言えないため解説します。

サービスアカウントによる認証

まずはGCPのコンソールからサービスアカウントを作成します。
https://console.cloud.google.com/iam-admin/serviceaccounts/

サービスアカウントの権限には「プロジェクトの編集者」の権限を付ける必要があります。

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

権限の設定が完了したら最後にjson形式で鍵を生成してください。

次にGoogle Developers Console API Library pageからGoogle Cloud Testing APICloud Tool Results APIを検索し、両方とも有効化してください3

jsonの鍵を用意できたらgcloudコマンドがサービスアカウントを使うように認証させます。

$ gcloud auth activate-service-account --key-file YOUR_SERVICE_ACCOUNT_KEY.json

gcloud auth listを実行してサービスアカウントにACTIVEが付いていればOKです。

この状態で再度gcloud firebase test ios runを実行して、まずはローカルの環境で実行できることを確認しておきましょう。

(サービスアカウントの認証がうまくいかない場合)

自分も実際に遭遇したのですが、上記の手順通りに作成したサービスアカウントでもなぜかFirebase Test Labを実行するときにエラーとなってしまう場合があるようです。
https://stackoverflow.com/questions/48327027/cant-run-firebase-test-lab-tests-using-gcloud-and-service-account-403-does-no?rq=1

上記のベストアンサーではない方の回答に、Firebaseのサービスアカウントの鍵を使うと動作したという回答がありました。Firebaseのサービスアカウントの鍵は以下のurlからアクセスできるページで作成できます。
https://console.firebase.google.com/u/0/project/FIREBASE_PROJECT_ID/settings/serviceaccounts/adminsdk
(FIREBASE_PROJECT_IDのところは自分のプロジェクトidに書き換えてアクセスしてください)

自分が試したところ、たしかに動作することを確認しました。詳しい理由は不明ですが、自分と同じようにGCPで作成したサービスアカウントではどうしてもエラーになってしまう場合に試してみるといいかもしれません。

CIサービスのビルドフローの設定

サービスアカウントの認証ができたところで、次はCIサービスのビルドフローに組み込んでみましょう。

お使いのCIサービスでgcloudコマンドを使えるようにし、先ほどと同様にサービスアカウントで認証をしてからテストを実行するフローを構築してください。少し注意が必要なのは、サービスアカウントの鍵のjsonです。 チームの体制にもよりますが、一般的にはリポジトリにコミットするべきではないでしょう。しかし、使用するCIの環境によっては秘密にしておきたいファイルをリポジトリとは別に登録するという方法ができない場合もあるかと思います。
そのような場合は、CircleCIのドキュメントにあるようにjsonの中身を環境変数として登録しておき、ビルドフローの中で再度jsonとして書き出すという方法が使えます。
Language Guide: Android - CircleCI

実際のサンプルとしてBitriseでgcloudを使ってサービスアカウントの認証、テストを実行する方法を紹介します。

まず、サービスアカウントの鍵であるjsonの中身をcatとpbcopyなどを使って丸ごとコピーして環境変数にセットしておきます。以下のコードでは$SERVICE_ACCOUNT_KEYで参照しています。

BitriseのScriptステップはシェルスクリプトをそのまま実行できるため、ここでインストールから認証まで行います。

#!/usr/bin/env bash

# Bitriseはデフォルトでgcloudがインストールされていないため、インストールから行う
curl https://sdk.cloud.google.com | bash > /dev/null 2>&1

# gcloudコマンドが使えるようにPATHを通す
source "${HOME}/google-cloud-sdk/path.bash.inc"
gcloud version

# サービスアカウントで認証
## 環境変数からjsonに復元する
echo ${SERVICE_ACCOUNT_KEY} > "${HOME}/gcloud-service-key.json"
gcloud auth activate-service-account --key-file "${HOME}/gcloud-service-key.json"
gcloud auth list

# プロジェクトidを設定
gcloud config set project ${GCLOUD_PROJECT}

後はテスト用のビルドを作成し、gcloud firebase test ios runを実行するだけです。テスト用のビルドについては説明済みですので、今回は省略します。

#!/usr/bin/env bash

# BitriseのScriptステップはPATHが引き継がれない(?)ようなので再度PATHを通す
source "${HOME}/google-cloud-sdk/path.bash.inc"

# Xcode Build for testing for iOSのステップでビルドすると$BITRISE_TEST_BUNDLE_ZIP_PATHというパスにzipされた成果物が保存されている
gcloud firebase test ios run --test "${BITRISE_TEST_BUNDLE_ZIP_PATH}" \
  --device model=iphone8,version=11.4 \
  --device model=iphonex,version=12.0,locale=ja_JP \
  --device model=iphone6splus,version=9.1 \
  --device model=ipad5,version=12.0 \
  --device model=iphone6s,version=10.3,locale=ja_JP,orientation=landscape

上記のサンプルには主にgcloudコマンドをセットアップするBitrise特有のコードも含まれてしまっていますが、サービスアカウントで認証してテストを実行する部分は他のCI環境でも同様に動作すると思います。

もしもより詳しいドキュメントを探す場合は、前例の多いAndroid版のFirebase Test Lab + CircleCIの組み合わせの記事を探してみるのがよいでしょう。

Fastlaneプラグインから使う

もしFastlaneを導入済みであれば、Firebase Test Labを実行するFastlaneプラグインを使用するという選択肢もあります。
fastlane/fastlane-plugin-firebase_test_lab

使い方はREADMEに書いてあるように、scanでテスト用のビルドを行ってからfirebase_test_lab_ios_xctestでテストを実行します。ただし注意点として、READMEのサンプルをそのまま使用した場合はgcloudのデフォルトの認証情報が使われます。つまりgcloudのコマンドから実行したときと同様に、CI環境では動きません。
oauth_key_file_pathというオプションにサービスアカウントの鍵のパスを指定することで、サービスアカウントで認証を行うことができます。

# fastlane-plugin-firebase_test_labのREADMEから転載
scan(
  scheme: 'YourApp',                  # XCTest scheme
  clean: true,                        # Recommended: This would ensure the build would not include unnecessary files
  skip_detect_devices: true,          # Required
  build_for_testing: true,            # Required
  sdk: 'iphoneos',                    # Required
  should_zip_build_products: true     # Must be true to set the correct format for Firebase Test Lab
)
firebase_test_lab_ios_xctest(
  oauth_key_file_path: '/your/servie_account/path.json', # このオプションにサービスアカウントの鍵のjsonのパスを指定する

  gcp_project: 'your-google-project', # Your Google Cloud project name
  devices: [                          # Device(s) to run tests on
    {
      ios_model_id: 'iphonex',        # Device model ID, see gcloud command above
      ios_version_id: '11.2',         # iOS version ID, see gcloud command above
      locale: 'en_US',                # Optional: default to en_US if not set
      orientation: 'portrait'         # Optional: default to portrait if not set
    }
  ]
)

ただし注意点として、FastlaneのプラグインはコミュニティによってメンテされているためXcodeのバージョンが上がった場合などに動かなくなる可能性があります。
実際に、Xcode 10.1でxctestrunの形式が微妙に変わったことで動かなくなってしまったバグの修正PRがついこの前マージされたようです。 このように何かがバージョンアップされた場合に壊れてしまうリスクがあるということは覚えておいた方がいいでしょう。

料金

最後は気になるお値段の方ですね。

まず、Bitriseで実行した場合は現在のところ無料です🎉
おそらく現在はまだβであることが無料の理由だと思いますので、今後どうなるかはBitrise Blogなどをウォッチしておくといいでしょう。

Bitriseを使わずに直にFirebase Test Labを使用する場合は無料プランと、従量課金プランがあります。

まず、無料のSpark Planの場合はテスト5回/日のようです。チームの規模が大きい場合には全てのpull-reqごとに実行するような使い方は難しいと思いますので、テストを実行するブランチを絞ったり、深夜ビルドでのみ実行するといった工夫が必要かもしれません。

従量課金制のBlaze Planにした場合、回数の上限はなくなり$5/デバイス/時間となります。これの意味するところは、テストを実行したデバイス全ての稼働時間の合計が1時間につき$5という意味です。 具体的な例としては、1回の実行に12分かかるテストを5台のデバイスで実行した場合、合計で1時間=$5と計算されるということになります。

ちなみに、料金はVirtual DeviceとPhysical Deviceに分かれていますがVirtual Deviceは今のところAndroidだけ存在しており、iOSの場合はPhysical Deviceのみとなります。

上記の料金の計算は執筆時点(2018年12月)のドキュメントを参考にしたものであり、必ずしも正しいことを保証するものではありません。
また、Firebaseの料金は今後変化する可能性がありますので詳しくはFirebaseの料金のページを参照するようにしてください。

まとめ

Bitrise、gcloudコマンド、FastlaneのそれぞれでFirebase Test Labによる実機テストを行う方法の解説をしました。Firebase Test Labはとても簡単に使えて、無料プランもあるのでこの手のサービスの中ではかなり試しやすいサービスだと思います。

ビルドのたびに様々なiOSデバイス、iOSバージョンの実機で自動テストを実行することが理想ですが、人手を介さずにそれを全自動で行ってくれるシステムを構築・運用することはとても難しいです。 そのようなシステム構築やデバイスの管理をする必要がないFirebase Test Labを使って、ゼロ円から実機の自動テストを始めてみませんか?


  1. Android版では実機だけではなくエミュレータを使用してテストを実行することも可能です。

  2. 実はGCPのプロジェクトIDでも実行することは可能ですが、テスト結果画面を見るにはFirebaseのプロジェクトページにアクセスする必要があるため、どのみちGCPのプロジェクトに紐づけたFirebaseのプロジェクトを作成する必要があります。

  3. ここで有効化しなかった場合でもサービスアカウントで認証後にTest Labを実行するとAPI [toolresults.googleapis.com] not enabled on project [****]. Would you like to enable and retry (this will take a few minutes)? (y/N)? というように有効化するか尋ねられるので、そこでyesとしても大丈夫なようです。