DeNA Testing Blog

Make Testing Fun, Smart, and Delighting End-Users

仕様記述テクニック「Promotion」の紹介

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

私はこちらの記事に記載の通り、形式手法の可能性を模索しています。 現在はツールやゲームの仕様を形式的に記述すること(形式仕様記述)で、仕様の欠陥をなるべく早く見つける取り組みにチャレンジしています。 今回は仕様記述をするにあたりよく使う重要な記述テクニックである「Promotion」を紹介します。

形式仕様記述とAlloyというツールを知っている人を対象にしています。 もし形式仕様記述やAlloyをご存じない方は、以前私がbuilderscon tokyo 2019で発表したときに使った資料をご覧ください。

Promotionとは

一般にソフトウェアシステムは複数のコンポーネントから構成されます。 システム全体としての状態(以下、システム状態)は各コンポーネントの状態の組み合わせからなります。 たとえどんなに奥深くのどんなに小さなコンポーネントが変化したとしてもそれはシステム状態の変化になります。

Promotionはコンポーネントの中で起こる局所的な変化をシステムレベルの大局的な変化に"昇格"させるテクニックです。 Promotionはシステム状態の変化といったグローバルな操作を、コンポーネントの変化といった「ローカルな操作」と 「ローカルとグローバルの状態の関係を表す述語」に分解して記述します。

分解せずに記述する方法と比べ、関心の分離ができる上、ローカルとグローバルの状態の関係を表す述語は再利用できるのでリファクタが容易になるといったメリットがあります。

一般的にPromotionは、ローカルの事前・事後を表す変数LocalVariablesLocalVariables'、ローカルの操作を表す述語PredLocal、ローカルとグローバルの状態の関係を表す述語PredPromoteを使い、次の形で表現されます。

\exists LocalVariables. \exists LocalVariables'. PredLocal \land PredPromote

これがグローバルな操作と同じ意味になります。

本記事の題材

題材としてGitを扱います。Gitのシステムはいくつかのコンポーネントからなります。 ここでは簡略化してシステムの状態とブランチ、コミット、ファイルの4つの関係を例に挙げます。次の図がその関係の例を示したものです。

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

ブランチ、コミット、ファイル枠内の丸1つ1つは、それぞれブランチ、コミット、ファイルを表します。
1つのブランチは1つの先頭コミットに紐付きます。あるコミットが、異なる複数のブランチに紐付けられることもありえます。 コミットは0以上のファイルに紐付きます。紐付けられたファイルは、差分ファイルではなくコミットに含まれているファイルを意味しています。
システム枠内の四角1つ1つは、Gitシステムが取りうる「状態」を表します(状態であることを区別するため丸ではなく四角で表現しています)。 状態は「どのブランチ・コミットの関係を持つか」、つまり、存在しているブランチそれぞれがどのコミットを指しているかによって決まります。 1つの状態に対し、「ブランチ・コミットの関係」は1つ以上紐付けられます。

今回はこのシステムを題材に「新しいファイルを追加するコミットを行う」を考えてみたいと思います。

システムの事前状態と事後状態

「新しいファイルを追加するコミットを行う」は、「コミットcに紐付いているある1つのブランチbが別のコミットc'に紐付く(今回はコミットの親子関係を考えない)」という状態変化があります。

図で表すとこのようなイメージです。

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

事前のシステムの状態をs、事後のシステムの状態をs'と表現しています。 ブランチbsに紐付けられたブランチです。sでは、ブランチbはコミットcに紐付きます。 「新しいファイルを追加するコミットを行う」を行った結果、s'では、ブランチbはコミットc'に紐付きます。 このとき、c'に紐付けられたファイルは、c'に紐付けられたファイルに新しいファイルを加えたものになります。

共通部分

上の図をAlloyで記述するのに必要な集合、関係は次の通りです。これはPromotionを使う・使わないに関わらず共通です。

sig System {
    bc: Branch -> one Commit
} {
    some bc
}

sig Branch {}

sig Commit {
    files: set File
}

sig File {}

Promotionを使わないストレートな書き方

Promotionを使った書き方を紹介する前にまずは条件をすべてそのまま書き下して書いてみます。

// システムの事前の状態: s
// システムの事後の状態: s'
// 向き先コミットが変わるブランチ: b
// コミットで新しく加わるファイル: newFile
pred commitNewFile(s, s': System, b: Branch, newFile: File) {

      // 【事前条件】追加されるファイルは元のコミットには含まれていない
      newFile not in (b.(s.bc)).files

      // 【事前条件】コミットに含まれるファイルの集合にnewFileを加えたものが新しいコミットのファイルの集合となる
      (b.(s'.bc)).files = (b.(s.bc)).files + newFile

      // s.bcのbの関係を上書き(b以外の関係は変えない)
      s'.bc = s.bc ++ b -> b.(s'.bc)

}

「b以外の関係は変えない」は(Branch - b) <: s'.bc = (Branch - b) <: s.bcとも書けます。

Promotionを使った書き方

先述の通り、Promotionはローカルな操作・ローカルとグローバルの状態の関係を表す述語でグローバルな操作を表現する書き方です。

\exists LocalVariables. \exists LocalVariables'. PredLocal \land PredPromote

Promotionを使って書いてみると次のようになります。

// もともと向いていたコミット: c
// 新しい向き先のコミット: c'
// コミットで新しく加わるファイル: newFile
pred commitNewFileLocal(c, c': Commit, newFile: File) {

    // commitNewFile内から必要なものを持ってくる
    newFile not in c.files
    c'.files = c.files + newFile

}

// システムの事前の状態: s
// システムの事後の状態: s'
// もともと向いていたコミット: c
// 新しい向き先のコミット: c'
// 向き先コミットが変わるブランチ: b
pred promoteCommitToSystem(s, s': System, c, c': Commit, b: Branch) {
    
    // 【事前条件】cはbがもともと向いていたcommitであること
    c = b.(s.bc)

    // システムの状態s.bc内のb->cをb->c'で上書き(b->c以外の関係は変えない)
    s'.bc = s.bc ++ b -> c'

}

// システムの事前の状態: s
// システムの事後の状態: s'
// 向き先コミットが変わるブランチ: b
// コミットで新しく加わるファイル: newFile
pred commitUsingPromotion(s, s': System, b: Branch, newFile: File) {
    some c, c': Commit {
        commitNewFileLocal [c, c', newFile]
        promoteCommitToSystem [s, s', c, c', b]
    }
}

それぞれの述語がどのような役割を担っているのか次の図をもとに説明します。

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

コミットとファイルに関わるローカルな操作(1の矢印)はcommitNewFileLocal で記述しています。

ローカルとグローバルの状態の関係を表す述語(2-a、2-bの線)はpromoteCommitToSystemで記述しています。 c = b.(s.bc)は、sの中のブランチbに対応するコミットとcが同じであることを意味しています(2-aの線)。 s'.bc = s.bc ++ b -> c'は2つのことを意味しています。1つはs'の中のブランチbに対応するコミットとc'が同じであること(2-bの線)、もう1つはb以外のブランチ(グレーのゾーン)が変化しないということです。 また、promoteCommitToSystemcがどのように変化するかは関与しません。 そのためpromoteCommitToSystemは他のローカル操作でも再利用ができます。

システムの事前状態sから事後状態s'に関するグローバルな操作(3の矢印)はcommitUsingPromotionで記述しています。 promoteCommitToSystemcommitNewFileLocalを組み合わせてグローバルな操作を表しています。

同値であることを確認

Promotionを使わない書き方とPromotionを使った書き方は同値なのでしょうか。2つの方法で確かめてみます。

Alloyのアサーションを使って確認

Alloyはアサーション検査ができるのでこの機能を使って確かめてみます。

assert promotion {
    all s, s': System, b: Branch, newFile: File {
        commitNewFile [s, s', b, newFile] <=> commitUsingPromotion [s, s', b, newFile]
    }
}

check promotion

検査をしてみましょう。

Executing "Check promotion"
   Solver=minisatprover(jni) Bitwidth=0 MaxSeq=0 SkolemDepth=1 Symmetry=20
   1032 vars. 60 primary vars. 2209 clauses. 113ms.
   No counterexample found. Assertion may be valid. 14ms.
   Core contains 4 top-level formulas. 12ms.

結果、反例は見つかりませんでした。 アサーションの検査で可能な有限の範囲に限れば、2つの表現は同値であることが確認できました。

同値変形を使って確認

別のアプローチとして同値変形を利用してみましょう。 まず、commitUsingPromotionの中のcommitNewFileLocalpromoteCommitToSystemを定義で置き換えてみます。

pred commitUsingPromotion(s, s': System, b: Branch, newFile: File) {
    some c, c': Commit {
        
        // commitNewFileLocalの定義
        newFile not in c.files
        c'.files = c.files + newFile
        
        // promoteCommitToSystemの定義
        c = b.(s.bc)
        s'.bc = s.bc ++ b -> c'
        
    }
}

限量子someが束縛する変数cc = b.(s.bc)なので、一点規則と呼ばれる次の規則が適用できます。

(\exists x. x = a \land P(x))  \Leftrightarrow  P(a)

すると次のように同値変形できます。

pred commitUsingPromotion(s, s': System, b: Branch, newFile: File) {
    some c': Commit {
        
        // commitNewFileLocalの定義
        newFile not in (b.(s.bc)).files
        c'.files = (b.(s.bc)).files + newFile

        // promoteCommitToSystemの定義
        s'.bc = s.bc ++ b -> c'
        
    }
}

また先述した通り、s'.bc = s.bc ++ b -> c'には「s'の中のブランチbに対応するコミットとc'が同じであること」という意味があります。 つまり、c' = b.(s'.bc)が成り立ちます。限量子someが束縛する変数c'についても一点規則が適用できます。

pred commitUsingPromotion(s, s': System, b: Branch, newFile: File) {
    
        // commitNewFileLocalの定義
        newFile not in (b.(s.bc)).files
        (b.(s'.bc).files = (b.(s.bc)).files + newFile

        // promoteCommitToSystemの定義
        s'.bc = s.bc ++ b -> b.(s'.bc)
        
}

本記事で一番最初に紹介したPromotionを使わない書き方commitNewFileと同じ形になりました。

まとめ

Promotionを使えばグローバルな操作をローカルな操作・ローカルとグローバルの状態の関係を表す述語に分けて書くことができます。 操作がローカルな部分のみを変更する場合はPromotionを検討してみると良いかもしれません。

参考

2019年最後のAndroid/iOS Test Nightを開催しました

SWETグループ、iOS自動テスト領域チームの平田(tarappo)とAndroid自動テスト領域チームの田熊(fgfgtkm)、外山(sumio)でお送りします。

おかげさまで、今年でiOS Test Nightは3周年を終えて4年目に、Android Test Nightは2周年を終えて3年目になりました。 皆様のおかげでTest Nightに登録されている資料は156件になりました。ここには素晴らしい知見が集まっています。

そんなTest Nightですが、12/16(月)に2019年最後のTest NightとしてiOSとAndroidを併せておこなったAndroid/iOS Test Nightを開催しました。 f:id:swet-blog:20191225153841j:plain

今年のTest Nightをすべて振り返りたいところですが、その中でも今年最後のAndroid/iOS Test Nightでの登壇について、SWETメンバーが軽くふりかえってみたいと思います。

Android枠の登壇内容

費用対効果の高いテストコードを書くために考えたこと


f:id:swet-blog:20191225150834j:plain:w380


この発表では、テストコードを書く目的の候補4つを挙げ、そこを出発点にテストを書く範囲を決めていく過程が解説されていました。

巷で良く言われている言説をそのまま適用せず、ご自身のプロジェクトの状況と照らし合わせて範囲を絞り込んでいく過程が丁寧に解説されており、とても共感できる内容でした。

絞り込んだ結果はKaoru Hotateさんのプロジェクトにしか当てはまらないとしても、その考え方は他のプロジェクトでも応用できると思います。これからテストを自動化しようと考えている方におすすめの発表でした。

AWS Device FarmとCircleCIでAndroidのUIテストを自動化しよう


f:id:swet-blog:20191225150609j:plain:w380


AWS Device Farmの機能と、Device Farmを活用したテスト実行をCIと統合する方法ついての紹介でした。 また、テストコードなしでデバイス上で自動テストが実行できるFuzzテスト機能のデモを見せていただきました。

Gradleのpluginがあるおかげで導入の敷居は低く、スモークテストをサッとはじめたいときにとてもいいなと思いました。

既存プロジェクトへCI/CDをどう導入するか?


f:id:swet-blog:20191225150811j:plain:w380


発表の中では、CI/CDの導入をただ単なる技術の導入ではなく、開発チームの文化を変えるきっかけにしている点が印象的でした。

さらに段階的に機能を追加していくうえで、Horie1024さんが実践してきた具体的なプラクティスについても紹介されていました。

SWETチームでもテストやCI/CDの導入を行うことがあるのですが、進め方を考える上で大変勉強になる発表でした。

iOS枠の登壇内容

Xcode 11におけるXCUITestの挙動


f:id:swet-blog:20191225153734j:plain:w380


Xcode 11ではいろいろな出来事が起きますが、その中でもXCUItestの挙動について話してくれました。

私も出会った事象はありましたが、ここまでいろいろな情報はさすがだなと思います。 Xcodeのメジャーアップデート時などにXCUITestにおいて何かしらの問題に出会ったら、Appiumのissueを見に行くのも1つの方法だと思います。

2019年のSwiftモック事情


f:id:swet-blog:20191226100522j:plain:w380


Swiftにおけるモック事情について複数のライブラリについて紹介してくれました。

本発表では、最近リリースされたMockoloについても触れていました。 このライブラリはUber製で、コード生成が高速であることがウリになっています。 SWETでも以前調査はしていたのですが、リリースされたばかりの1.1.0については追加調査をしておらず、本発表で改めて試してみないといけないなと思いました。

DeNAからの登壇内容

DeNAからはMOVのiOSアプリ開発をおこなっているsatoshin21とSWETから平田が登壇しました。

GTXiLibで小さく始めるAccessibility Testing


f:id:swet-blog:20191225153801j:plain:w380


Accessibilityは重要という認識は広まってきてはいますが、優先順位的に下がりがちではあります。 ただAccessibility Inspectorを使って、常にチェックをするのはなかなか高コストです。 そこで、GTXiLibを使ってテストを小さくはじめようという内容です。

最近、この手の話を聞く機会も増えており重要性がさらに高まってきたと思っています。 私はまだ手を出せていませんが、アクセシビリティについてもテストをしていく必要性を感じています。

iOSにおけるパフォーマンス計測


f:id:swet-blog:20191225153821j:plain:w380


Appleはここ数年パフォーマンスに関する機能を提供してきています。 また、パフォーマンス改善に関するドキュメントなども提供しています。

iOSにおいてはどのように改善サイクルを回すと良いのか、またその時に利用できるもgのとして何があるのかについて登壇をしました。

最近、パフォーマンス周りについて力を入れていることもあり今回のような登壇をしました。 お話しきれなかったことも多くありましたので、また何かしらの形でアウトプット出来ればと思います。

ブログ枠のまとめ

ウホーイさんが作成してくれました。 各発表のTwitterでの反応がまとまっており、当日の盛り上がりが伝わってきます。 いつもありがとうございます。

最後に

2019年、皆様のおかげで素晴らしいTest Nightを合計9回も開催することができました。 2020年もTest Nightなどを通してさらなるナレッジを皆様と一緒に世に届けられたらと思います。

最後の最後に、 SWETの仕事に興味を持った方は、SWETメンバーに声をかけていただければと思います。

モブプログラミングワークショップを開催した話

この記事は、モブプログラミング Advent Calendar 2019の20日目の記事です。

SWETグループの長谷川(@nowsprinting)です。

先日、AGILE MONSTERの及部さん(@TAKAKING22)をファシリテーターにお招きし、SWETでモブプログラミング(以下モブプロ)のワークショップを開催しました。

SWETの取り組みの中には、DeNA内の多様なプロダクト開発チームに対してテスト技術を伝える・サポートすることも含まれています。 その活動においてモブプロは有効な手段ではないかと考え、より効果的なセッションを運用するためのノウハウを得たいというのが目的です。

この記事では、ワークショップを通して感じたことなどをまとめて紹介します。

ワークショップ

ワークショップ内容は、及部さんの『Head First モブプログラミング』をカスタマイズした資料でのイントロダクション、モブセッション、発表会、振り返り、『小さなチーム、大きな仕事を実装するモブプログラミング』+αの講義、という構成でした。

+αの部分では、我々SWETが今後モブセッションをファシリテートしていくための知見を話していただけるようリクエストしていましたが、そのアンサーは

  • ファシリテートしない
  • カオスを受け入れる

の2点でした。 言葉だけ見ると突き放すようなインパクトがありますが、実際にワークショップを通して聞くと納得感を得られるものでした。

この2点のアンサーを含め、ワークショップ開催にあたって及部さんが整理されたであろう内容がアドベントカレンダー1日目の記事『モブプログラミングを導入するときに考えていること』に書かれていますので、ご興味ある方はあわせて参照をおすすめします。

モブセッション

モブセッションでは、3〜4人ごとのチーム(モブ)に別れ、それぞれ1台のPCで「自動販売機の機能を実装する」という課題に取り組みました。

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

以下、私のいたモブの話をします。

課題の「自動販売機」に対し、MVP(Minimum Viable Product)だけ定義してあとはTDD(Test Driven Development:テスト駆動開発)でやりましょう! と、一番早くホワイトボードから離れ作業に入りました。 しかし進めるうち、ストーリーでなく機能に目が行ってしまったり(例えば「品切れ」などの考慮をしはじめたり)、コレクションをどこに持つかのコンセンサスが取れていなくてホワイトボードに戻ったりと迷走し、最初のテストが通ったところで前半セッションを終了。

休憩後の後半から、ようやくTDDが回りだしたものの、最後にテストを壊したところでタイムアップ。成果物としては悔いの残る結果に終わりました。

タイピスト(ドライバー)交代にはMobsterを使いましたが、要所要所でコミットをちゃんと残すことに意識が行かず(結局はじめにgit initしただけでノーコミット)、実際の業務でモブプロする場合には別の交代契機を検討したほうが良さそうという学びを得ました。

個人の感想とまとめ

これまでは、モブプロの業務効率についてはあまり意識していませんでした。 しかしワークショップを通じて、通常の分担作業前後のミーティングがモブプロに包括される効果やフロー効率の良さを感じられ、普段からモブプロで業務を進めるという選択も現実的ではないかと思うようになりました。

特に、SWETとしては普段から自動テストの効用として、不具合を早期に発見にすることの価値を伝えていますが、モブプロによる細かい軌道修正は同様の効果がありそうです。

ほかに印象に残ったのは、同じSWETとはいえ普段同じプロジェクトで仕事をしているわけではないので、お互いのテストの書き方の違いを見られる・教え合えるのは新鮮で楽しい体験でした。 Pull Requestレビューでは経緯や細かい点までわざわざ書かないものなので、ペアプロ/モブプロをしないとわからなかったことでしょう。

今後の取り組み

テストなどのノウハウの共有を目的とするモブプロは、ペアプロに誘うより心理的障壁も低く、セッション中の負荷も軽いため、積極的に使っていこうと試行をはじめています。 そのために、(普段のチーム開発ではなく)アドホックに実施するモブプロに最適なテンプレート的なものの整備も進めています。

またモブプロでは、エンジニア経験が少ない人や他の職種の人であっても、まわりがナビゲートすることでタイピストが務まります。

ゲーム開発ではエンジニア以外の様々なロール1と連携して開発を進めていきますが、ところどころでモブプロに参加してもらうことでお互いの考えを知り、暗黙知を共有できるのではないかと考えています。 当初想定していた「テスト技術を伝える」ためだけではなく、モブプロそのものの良さを伝え、社内各所で試してもらえるように活動していきたいです。

以上のような試みに共感していただけた方、興味を持たれた方、一緒に働いてみようと思ってくれた方。 下記職種で採用しておりますので、ぜひご応募ください。お待ちしております。

テスト自動化エンジニア(Unity Test)

テストエンジニア (ゲームアーキテクチャ)

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


  1. プロデューサー、ディレクター、プランナー、サウンドディレクター、UIデザイナー、エフェクトデザイナー、グラフィックスプログラマー、QAなど

SWETの2名が執筆に加わった「iOSテスト全書」が一般発売されました

SWETグループ、iOS自動テスト領域チームの平田(tarappo)です。

長いことおまたせしましたが、12/16(月)に「iOSテスト全書」が一般発売されました。 私と同じくSWETの細沼(tobi462)が執筆に加わっています。

f:id:swet-blog:20191216185012j:plain ※12/16(月)に開催されたiOS / Android Test Nightの開始前に写真を撮りました。

本書の一般販売を記念して、本エントリーでは本書全体の概要と見どころについて解説します。 また、併せてすでに発売済みの「iOSアプリ開発自動テストの教科書(以後、教科書本)」との想定読者の違いについても説明します。

本書の企画

本書の企画は、私が相談をして進めさせてもらいました。

昨年(2018年)「Androidテスト全書」の執筆に関わり、周りからの意見もありiOSアプリ開発においても、このようなテストについての本の必要性を強く感じていました。

iOSアプリ開発においては、テスト周りにおいて体系的にまとまった情報はあまりありません。 一歩踏み込んだ情報となると、日本語でのネット情報は少なくその情報自体最新のものなのかわからないといった状況でもありました。

「iOSテスト全書」の企画を立ち上げる前から、私と細沼は「教科書本」の執筆について話が進んでおり執筆がスタートしはじめていました。 この本はその名の通り教科書として最初の一歩として道標になると思ってはいたものの、この本だけでは不足している面もあるだろうとも思っていました。

そこで、「iOSテスト全書」としてさらに一歩の情報まで載せた本を世に出したいと思い、執筆に加わってほしい人たちに声をかけました。

Androidテスト全書が一般発売されたのは2018年11月5日になります。 そこから2週間後、11月19日が執筆者と編集者が揃った「iOSテスト全書」のキックオフの日になります。 そして2019年1月28日にクラウドファンディングがスタートし、このたび一般販売となりました。

思っていたよりも時間がかかってしまいましたが、良いものができたと思います。 ぜひ、手にとっていただければと思います。

本書の概要

本書の目次は次のとおりです。

  • 1章:テスト自動化入門
  • 2章:ユニットテスト(概要)
  • 3章:ユニットテスト(XCTest編)
  • 4章:ユニットテスト(Quick/Nimble編)
  • 5章:BDDによるアプリ開発
  • 6章:UIテスト入門
  • 7章:XCUITestを使ったUIテスト
  • 8章:サードパーティ製ツールとAppium
  • 9章:CI/CD

自動テストに対する話から、ユニットテスト・UIテスト、そしてそれらを実行するCI/CDについてまで触れています。 この中から細沼が2章〜5章(3章共著)、平田が6章と9章を担当しています。

「教科書本」はテストに関するものを幅広く扱い、その名前の通り教科書として最初に活用してもらうことを想定しています。 「iOSテスト全書」では、より一歩進んだところにもいけるよう自動テストやCI/CDについてもう少しふみこんで書いてあります。

このようなiOSアプリ開発におけるテスト周りにフォーカスした本が2019年に2冊も出たということは凄いことだと思っています。 是非ともiOSアプリ開発をおこなっている方は、本書を手にとって頂いて書いてある内容にチャレンジしてみてください。

本書の想定読者

本書の想定読者はPEAKSのページに書いてあるとおり次の方になります。

  • テストについて興味はあるがどうしたらいいかわからない方
  • テストについてもっとよい書き方があるのではないかと悩んでいる方
  • テストにもっと強くなりたい方
  • CI/CDサービスをもっと活用したいと思っている方

iOSアプリ開発に携わる人であれば、なにかしら得られるものがある内容になっています。

各章の見どころについてはそれぞれの執筆者が書いてくださると思っています。 ここでは、我々が執筆した章についての見どころについて次に説明していきたいと思います。

2章〜5章の見どころ(細沼担当)

個人ブログ記事でも書かせていただきましたが、 私が担当した2章、4章、5章はそれぞれ明確なコンセプトをもって執筆をさせていただきました。

2章は『iOSに限定されないユニットテストの知識』をテーマにして、 ユニットテストにおいて一般的に利用できるテクニックを紹介しています。

個人的な見どころは『テストコードの保守性を上げる』のトピックです。 XCTestを使った愚直なテストコードの例からはじめ、 テストコードを徐々にリファクタリングしながら保守性を上げる過程がわかるようになっています。

3章は共著として一部のみを担当しました。 具体的には『テスト実行の単位を管理する方法』として、 これまで利用されてきた『スキーム』を使った方法に加え、 Xcode 11から追加された『テストプラン』の利用方法について記載しています。

このあたりは中々分かりづらい機能ですが、 個人的にわかりやすく解説できているのではと感じます。

4章は『日本語における最強のQuick/Nimbleリソース』をテーマとして執筆しました。

Quick/Nimbleは公式でもドキュメントが整備されているOSSですが、 知識を体系的に積んで学ぶという点においては、個人的に不足を感じていました。

そこで知識を一から積んでいけるように構成を工夫し、 分かりづらい機能についても、できるだけ丁寧な解説を心がけています。

5章は『私が今まで得てきた知恵や経験を、書籍を通して読者に』をテーマにし、 Quick/NimbleとBDD(振舞駆動開発)を利用してサンプルアプリを開発する内容になっています。

2章〜4章で解説してきた静的なテクニックをつなぎ合わせ、 動的なストーリーとして理解を深められる構成になっています。 そういった意味で、2章〜4章で学んできた内容を締めくくる、ユニットテストにおけるまとめ的な章になっています。

6章、9章の見どころ(平田担当)

6章の「UIテスト入門」については、UIテストをおこなっていく上で、次のようなフェーズ単位で気にすべきことをまとめました。

  • 導入前に検討したほうがいいこと
  • 実装中に考慮すべきこと
  • 運用時に注意するべきこと

UIテストは実装して、動いているのを見ると「なんか凄い!」と実装者自身やチームメンバーが感じやすいです。 その結果、闇雲にテストコードを追加し、その結果として負債となるコードが生まれることもあります。

一旦落ち着いて何をするべきかを考えることが重要です。 そこで筆者の経験などを元に、UIテストが実際に現場で活用できるように、それぞれのフェーズで気にすべきことをある程度まとめました。

9章の「CI/CD」では、「CI/CDとはなにか」について厚めに述べています。 本来は、この話だけで1冊になってしまうぐらいの話ですが、次のようなトピックに注力し説明をしています。

  • CIとはなにか?
  • CDとはなにか?
  • どのようなワークフローを組むか

このような内容は、特定のサービスに限らず他でも汎用的に利用できるものです。

しかし、実際に利用した形も見ないとイメージが分かりづらいのも事実です。 そこで、述べたワークフローの例をもとにiOSアプリ開発で人気のCI/CDサービスであるBitriseを用いて説明をしています。

本章の内容により、自社のプロダクトにおいてどのようなワークフローを用意するのが良いかを考える一助になってくれると筆者としては嬉しい限りです。

おわりに

今回は、一般発売がはじまった「iOSテスト全書」について解説させていただきました。 この本が何かしら皆さんにとってプラスとなり、そして今後テストに関する情報がいろいろな媒体を通して世に出てもらえれば筆者としては嬉しい限りです。

また、我々SWETとしては今後「教科書本」や「iOSテスト全書」を社内で活用するよういろいろとおこなっていく予定です。 まだ何かしら具体的な活動はそこまで行えておらず、世へのアウトプット出来ていませんが、今後のアウトプットをお待ちいただければと思います。

そして、これらの活動に興味がある方はぜひとも次から書かれている内容を元に仲間となっていただけると嬉しい限りです。

仲間を募集中

SWETチームでは自動テストやCI/CDを活用する仕事に興味を持った方を募集しています。

以下のエントリフォームから応募できるほか、 私や他のSWETメンバーに声をかけて頂くかたちでも大丈夫です。

是非ともお気軽にご連絡ください!

xcode-installのリグレッションテストをGitHub Actionsで自動化した話

この記事はDeNA Advent Calendar 2019の2日目の記事です。

SWETの加瀬(@Kesin11)です。

SWETグループではいくつかの分野ごとにチームを分けて活動をしており、自分は現在CI/CDチームで主にゲーム事業部向けのJenkinsやCI/CDのサポートをしています。

今回は、xcode-installというgemにpull-reqを送る際に、動作確認のための環境としてGitHub Actionsを活用したことについての記事です。

xcode-installとは

xcode-installはその名の通り、XcodeのインストールやアンインストールをCLIから可能にするxcversionというコマンドを提供するgemです。大半の方はApp StoreやAppleのデベロッパー向けサイトからXcodeをダウンロードして手動でインストールしているでしょうから、そのようなツールがなぜ必要なのかと不思議に思われるかもしれません。

iOS開発におけるCI/CDには、CircleCIやBitriseといったマネージドサービスを利用することが一般的になってきました。ですが、ビルド時間やリポジトリ容量などの問題からそのようなサービスを使いにくいプロダクトも存在します。その場合は、Jenkins + ビルド用macOSの両方を自前で構築、管理することになります。

このビルド用macOSのマシンには、iOSアプリをビルドするための様々なツールを予めセットアップしておく必要があり、Xcodeもその1つです。マシンが1台だけであれば手動で全てのツールをセットアップ可能ですが、ビルドマシン増設を容易にするためSWETではAnsibleを採用してビルドマシンの各種ツールのセットアップを自動化しています。

Ansibleでツールをインストールする際には、homebrewのようなCLIツールが提供されていると簡単です。そこで、Xcodeをインストールするにはxcode-installが必要不可欠となります。

ここからが本題となるのですが、Xcode 11がリリースされてほどなくした今年の10月末にxcode-installで前バージョンであるXcode 10.3をインストールしようとしたところ、なぜか失敗するようになっていました。xcode-installが使えなくなるのは自分たちの業務に大変影響があるため、原因を調査して修正することにしました。

Xcode 10.x系がインストールできない問題(修正済み)

今回、自分が遭遇した問題はXcode 10系でインストールに失敗するというものでした(issue)。ちょうどXcode 11系の正式版がリリースされた時期で、Xcode 11系のインストールは問題がなかったためか、自分以外にissueで報告している人もいないようでした。

ソースコードや過去のpull-reqを追ったところ、ダウンロードしたxipを解凍後のXcode.appの証明書検証に失敗していることが分かりましたので、手元でインストールしたgemに対して後にpull-reqを出したパッチを当てて、Xcode 10.3のインストールが成功するように修正をしました。

xcode-installを修正する難しさ

自分は数年前からxcode-installを使用していますが、過去にも度々動作が不安定になったり、特定の人の環境では失敗するという話を社内で聞くことがありました。不安定な要因として、xcode-installに関係する環境の複雑さがあります

  • macOSのバージョン
  • Rubyのバージョン
  • Fastlaneのバージョン1
  • インストールしようとしているXcodeのバージョン

xcode-installの過去のissuepull-reqを見てもらうと分かるのですが、自分の環境では動いた/動かなかった、という報告が多いです。

今回の自分の修正パッチも、自分の手元のmacではXcode 10.3がインストールできることは確認したものの、 Xcode 10.0 ~ 11.1の間に含まれる他のバージョンで逆にインストールが失敗してしまうのではないか不安がありました。

本来であれば全てのバージョンで動作確認をするべきなのですが、Xcodeのインストールは自分の環境で1回あたり30-40分程度の時間がかかることと、xipをダウンロード後の解凍や検証の処理でCPUパワーが使われるためになかなか辛いものがありました。

GitHub Actionsを利用したリグレッションテスト

手元のmacで実行するのが難しいのであれば、外部のマシンパワーをお借りしたいところです。ちょうどGitHub Actionsがオープンβで個人的にも色々試していた時期で、macOSのVMも使えることが分かっていました。このmacOSのVMを利用してxcode-installの過去のXcodeバージョンのインストール動作確認、すなわちリグレッションテスト(回帰テスト)を行うことを思いつきました。

自分が修正パッチを当てる前は、10系のXcodeのインストールが失敗するという状態でしたので、実際に全てのXcodeのバージョンについて問題なくインストールできることを確認することにしました。確認の手順としては以下になります。

  1. xcversion install {XCODE_VERSION} が終了コード0で完了すること
  2. xcversion installed の結果にインストールしたXcodeのバージョンが表示されること

実際にGitHub Actionsのyamlに書き下したのがこちらです

name: "E2E test"
on: [pull_request]

jobs:
check_install:
  strategy:
    fail-fast: false
    matrix:
      os: [macos-latest]
      xcode: ["10.0", "10.1", "10.2.1", "10.3", "11.0", "11.1", "11.2.1"]

  runs-on: ${{ matrix.os }}
  env:
    # It needs AppleID that has disabled 2FA.
    XCODE_INSTALL_USER: ${{ secrets.XCODE_INSTALL_USER }}
    XCODE_INSTALL_PASSWORD: ${{ secrets.XCODE_INSTALL_PASSWORD }}
  steps:
  # Prepare env
  - uses: actions/checkout@master
  - uses: actions/setup-ruby@master
    with:
      ruby-version: '2.6'

  # Show env
  - name: Show macOS version
    run: sw_vers
  - name: Show ruby version
    run: |
      ruby --version
      bundler --version
  
  # Prepare
  - run: bundle install -j4 --clean --path=vendor
  - run: bundle exec xcversion update
  - name: Show installed versions before install
    run: bundle exec xcversion installed
  - name: Uninstall installed target Xcode version
    run: bundle exec xcversion uninstall ${{ matrix.xcode }} || true

  # Exec
  - run: bundle exec xcversion install ${{ matrix.xcode }}

  # Check
  - name: Show installed versions after install
    run: bundle exec xcversion installed
  - name: Check Xcode installation was successful
    run: bundle exec xcversion installed | grep "${{ matrix.xcode }}"

xcode-installをforkした自分のリポジトリでこのGitHub Actionsを動かし、全てのバージョンで問題なくインストールできることを確認できましたので、自信を持って本家にpull-reqを送ることができました。

GitHub Actionsは他のCIサービス同様に、publicなリポジトリであればオーナー以外のユーザーも実行ログを確認できます。自分は今回pull-reqを出した側でしたが、レビュワーも実際に動いたログからデグレしていないことを確認できたため、安心できたのではないでしょうか。

その後、自分が作成したGitHub Actionsのコードはメンテナの方に興味を持ってもらえたようで、別のpull-reqで本家にマージされました🎉
xcode-installの動作確認が簡単になることで他の方もコントリビュートしやすくなり、今後は安定性も向上するでしょう。

まとめ

今回はxcode-installという特殊なツールの修正事例を通してGitHub Actionsの少し変わった活用方法をご紹介しました。GitHub Actionsでは他のCIサービスと同様にLint、ユニットテスト、デプロイというフローを組み立てるのが一般的だと思いますが、応用次第で今回のように色々と面白いことができるはずです。

最後に、CI/CDチームでは社内におけるCI/CD環境の開発・提供を通じて、開発者が開発に専念できるプロセスを支援しています。
興味を持たれた方は下記の職種で採用をしておりますので、ぜひご応募ください。


  1. Developer Portalにアクセスする処理などにFastlane内部のライブラリが使用されていたりします。ですが、xcode-installのgemspecでFastlaneのバージョンは厳格に管理されていないため、環境によってはとても古いFastlaneが使われてしまうことで問題となることがあります

MOV Android版に対する「コード改善+テスト導入」の取り組みの紹介

こんにちは。SWETの瀬戸(@seto_hi)です。

2019年7月下旬からSWETにジョインし、テストが書きにくい設計を改善して自動テストの導入をサポートする取り組みを行っています。 その取り組みの一環として、MOV Androidアプリの設計の改善を進めてきました。

本記事では、2か月間の中で改善した4つの事例について紹介したいと思います。

MOVのAndroidアプリについて

DeNAでは MOV というタクシーの配車サービスを運営しています。
今回リファクタリングを行ったのは、アプリのユーザーが配車依頼から支払いまでを行う、アプリの中心となる画面です。

画面構成

画面の構造は下記のようになっています。

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

常時表示されるActivityと地図Fragmentはシンプルですが、OverlayFragmentの構造がとても複雑な作りとなっています。 OverlayFragmentは20種類以上の状態があり、その状態に従って10種類以上のFragmentが差し替わるようになっています。
それだけでなく、OverlayFragmentの変更を受けてActivityと地図Fragmentの状態も変更されるので、状態の伝搬や管理が複雑になっています。

実装上の問題点と解決策

画面構成の複雑さだけでなく、MOVのユーザーアプリには様々な歴史的経緯があり内部実装は更に複雑になっていました。 特に、ActivityとFragmentは見事なFatActivityとFatFragmentとして育っており、修正コストの増加やテストコードの書きづらさといった課題につながっていました。

原因を分解し、それぞれについて解決策を考えました。

課題1:BaseFragmentと多数のopenメソッド

BaseFragmentに空実装のopenメソッドが大量に定義され、各Fragmentはそのメソッドを必要に応じてoverrideしており見通しが悪くなっていました。
多くのメソッドはイベントをトリガーとして呼ばれるものだったため、ViewModelにイベント用のLiveDataを用意し、各FragmentでobserveすることによりBaseFragmentへの依存を減らしました。

課題2:Activity、地図Fragment、OverlayFragment間の不必要な依存

地図の操作イベントはCallback経由で一旦Activityに集約され、再度地図FragmentやOverlayFragmentに分配されていました。
Activityに処理を集約する必要は全くなかったので、ViewModelのLiveData経由で地図Fragmentに処理をさせるように変更しました。
OverlayFragmentの画面遷移を知らせるイベントもCallback経由で行っていたのですが、NavController.OnDestinationChangedListenerを使い、依存と実装量を減らすことができました。

これらの修正によってActivityがFragmentのインスタンスを持つ必要がなくなり、今後不必要な依存を作ってしまう可能性が減りました。

課題3:APIコールとCallbacksの実装がActivity/Fragmentにべた書きされている

APIコールとCallbacksの実装がActivity/Fragmentに書かれていたため、Activity/Fragmentの行数が増えるだけでなく、Callback内の処理のテストを書くことが難しくなっていました。

これを解消するため、Googleの提唱する Recommended app architecture に従い、設計を変更しました。
ViewModelでAPIコールをし、Coroutine化したことでcallbackも少ない行数で実装ができています。

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

ActivityやFragmentが状態を持っていた設計をRecommended app architectureに適合するにあたり、今回は3段階のステップを踏みました。

  1. Repositoryを作成することでViewModelへの移植をしやすくする
  2. Coroutine化でCallback処理の整理をする
  3. 状態をLiveData化し、ViewModelへ移植する

依存を減らしつつ移植することにより、安全な設計変更を行うことができました。

改善後のプロダクトコードとテストコードは以下のようになりました。

サンプルコード

CarRequestRepositoryにはCarRequestを返すgetCarRequest()というメソッドがあります。
(ここでは簡易的な例として、localにデータをキャッシュしないものとします)

class CarRequestRepository(
  private val remoteSource: CarRequestRemoteDataSource
) {
  suspend fun getCarRequest(): CarRequest = 
      remoteSource.getCarRequest()
}

ViewModelではこれを読み込み、LiveDataに保存します。 必要であればActivityやFragmentでLiveDataをObserveして値を使うことができます。

class CarRequestViewModel(
  private val repository: CarRequestRespository
): ViewModel {

  private val _carRequest = MutableLiveData<CarRequest>()
  val carRequest: LiveData<CarRequest> = _carRequest
  
  fun loadCarRequest() {
    viewModelScope.launch {
      _carRequest.value = repository.getCarRequest()
    }
  }
}

テスト

このような設計だとテストも書きやすくなります。
CarRequestViewModelのコンストラクタ引数としてCarRequestRespositoryを渡しているため(コンストラクタインジェクション)、モックオブジェクトに差し替えることが容易になります。

CarRequestViewModel#loadCarRequest呼んだ際、取得結果をobserverで検知できることを確認するテストは以下のようになります。

class CarRequestViewModelTest {
  
  @Test
  fun testLoadCarRequest_success() {
    val mockRepository = mockk<CarRequestRepository>()
    val target = CarRequestViewModel(mockRepository)
    val result = CarRequest()

    // coEveryはsuspend fun向けのevery
    // mockRepository.getCarRequest()が呼ばれたらresultを返すように設定する
    coEvery { mockRepository.getCarRequest() } returns result

    // ViewModelのcarRequestが変更されたことを確認したいのでobserverをmockする
    val mockObserver = spyk<Observer<CarRequest>>()
    target.carRequest.observeForever(mockObserver)
    // テスト対象のメソッドの呼び出し
    target.loadCarRequest()

    // mockObserverがmockRepository.getCarRequest()の結果で呼ばれたことを確認する
    verify(exactly = 1) {
      mockObserver.onChanged(result)
    }
  }
}

課題4:DialogやDrawerMenuのCallback実装もActivity/Fragmentで行っている

こちらも課題3と同様にCallbacksの実装がActivity/Fragmentに書かれていたため、Activity/Fragmentの行数が増えるだけでなく、Callback内の処理のテストを書くことが難しくなっていました。

こちらはEnumを利用することで課題を解決しています。

サンプルコード

まず、メニューの各項目をEnumとして定義します。
MOVのアプリではメニューの選択時に新しいActivityを開くようになっているため、Intentの生成メソッドも定義しました。

enum class DrawerMenuItem(
  @IdRes private val menuId: Int
) {
  ACCOUNT(R.id.menu_account) {
    override fun createIntent(context: Context): Intent =
        AccountActivity.createIntent(context)
    },
...

  abstract fun createIntent(context: Context): Intent
}

取り回しを便利にするため、idを元にEnumの要素を取得できるメソッドを定義します。

  companion object {
    fun valueOf(@IdRes id: Int): DrawerMenuItem =
        values().firstOrNull { it.menuId == id }
            ?: throw IllegalArgumentException()
  }

こういった実装をすることにより、Activity側では以下のように記述するだけで各メニューの選択時の処理ができるようになります。
将来的にメニュー項目が増えても、Activity側に修正を入れる必要はありません。

  navigation.setNavigationItemSelectedListener { menuItem ->
    val intent = DrawerMenuItem.valueOf(menuItem.itemId)
           .createIntent(this)
    startActivity(intent)
    false
  }

ちなみに、このようにEnumを使った手法はActionMenuやActivityResultにも応用できます。

テスト

enum化することによりテストも書きやすくなります。
DrawerMenuItem.ACCOUNT.createIntent のテストは以下のようになります。

  @Test
  fun createIntent_account() {
    val intent = DrawerMenuItem.ACCOUNT.createIntent(context)

    assert(intent.component).isNotNull()
    assert(intent.component!!.className).isEqualTo(AccountActivity::class.java.name)
  }

最後に

上記のような修正によって、アプリの設計は改善の方向に進み、テストコードも書きやすい状態に変化していきました。 このようなアプリの設計改善に限らず、SWETチームでは引き続き自動テストを導入するための取り組みを進めていきます。

このような取り組みに興味を持たれた方は、下記URLからのご連絡をお待ちしております!

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

Xcode 11でのテスト周りの新機能を紹介します!

SWETグループ、iOS自動テスト領域チームの細沼(tobi462)です。

今回はWWDC19で発表された内容の中から、 Xcode 11におけるテストまわりの新機能について紹介します!

新しく追加された機能は大きく以下の3つです。

  • Test Plans(テストプラン)
    • テスト実行の設定(実行対象、言語・ロケール、他)を管理できる仕組み
  • Result Bundle
    • テストの成果物(ビルドログ、テストレポート、他)をまとめる仕組み
  • XCTest - XCTUnwrap()
    • XCTAssertNotNil + guard letに相当する関数

なお、メトリクスまわりも大きく進化していますが、本記事では割愛します。

関連するWWDC19のセッション

それぞれの機能について解説する前に、 関連するWWDC19のセッションを紹介したいと思います。 本記事で解説する自動テストに関するセッションに加え、 デバッグやメトリクスが解説されているセッションもあわせて紹介します。

なお、現在では日本語字幕にも対応しています。 どれも素晴らしいセッションとなっているので、 まだ見られていない方は是非ご覧ください。

Testing in Xcode(#413)

今回のWWDC19において、自動テストをテーマとした最も大きなセッションです。

セッションは自動テストのバランスを考える上で大切な「テストピラミッド」の紹介から始まり、 XCTestやカバレッジなどの基本的な利用方法についての説明を経て、 新機能である「Test Plans」や「Result Bundles」の解説がされます。

またセッションの最後では、 自分でCI環境を構築する方法についても解説され、 とても盛り沢山なセッションとなっています。

セッション中のデモがとても分かりやすく、 テストをまだ書いたことがない人にも是非見ていただきたい内容です。

Debugging in Xcode 11(#412)

Xcode 11で追加されたデバッグまわりの機能について、解説されたセッションです。

SwiftUIまわりのデバッグ機能についての紹介が中心となっていますが、 通信環境や端末温度といったデバイス状態について、 上書きしてエミュレートする機能なども紹介されています。

デバッグは自動テストと直接関係するわけではありませんが、 アプリを効率的に開発すするための必須テクニックとして、 iOSアプリ開発者であれば是非とも抑えておきたい機能です。

What's New in Xcode 11(#401)

Xcode 11の新機能について、解説されたセッションです。

自動テストとは直接関係ないセッションですが、 「Test Plan」や「デバイス状態の上書き」といったデバッグ機能について紹介されています。 Testing in Xcode(#413)Debugging in Xcode 11(#412) といったセッションとあわせて見るとより理解が深められる内容かと思います。

Xcode 11では「エディタ分割の強化」や 「Swift Package Managerのサポート」など、 テスト以外についても多くの機能が追加されています。

iOSアプリ開発者であれば必見のセッションといえます。

Improving Battery Life and Performance(#417)

アプリのメトリクス取得について解説されたセッションです。

メトリクス取得についての基本的な考え方から、 XCTestに追加されたAPI、 新しく追加されたMetricKitなどについて解説されています。

冒頭で記載したように、 本記事ではメトリクス取得については触れませんが、 興味のある方は是非このセッションをご覧ください。

Xcode 11におけるテスト周りの新機能

それではXcode 11で追加されたテスト周りの新機能について、次の順で解説していきたいと思います。

  • Test Plans(テストプラン)
  • Result Bundle
  • XCTest - XCTUnwrap()

Test Plans(テストプラン)

Test Plansはその名のとおり、 テスト実行における計画(テスト対象、コンフィギュレーション)を管理できる機能となっています。 WWDC19のセッションでは具体的な利用例として、 多言語(日本語、英語、など)のテストなどが紹介されていました。

従来はスキームを利用して実現していたものですが、 スキーム設定ではビルドの管理といった別の用途に利用されるため、 こうしたテスト実行の管理をするための仕組みとしては煩雑でした。

今回それらが「Test Plans」という形でスキームから独立し、 テスト実行の設定を直感的に分かりやすく管理できるようになりました。

まさに、「Test Plans(テスト計画)」という名に恥じない機能となっています。

Test Plansを導入する

既存プロジェクトにTest Plans導入するには、 既存のスキーム設定から変換する方法が簡単です。

変換はスキーム設定>Test>Infoタブにある「Convert to use Test Plans...」ボタンから行います。

ダイアログが表示されるので、スキーム設定から変換する場合は一番上を選択します。

任意のファイル名(今回は AllLanguage.xctestplan)で保存すると、Test Planに切り替わります。

Test Plansの設定

プロジェクトナビゲータに追加された.xctestplanを選択すると、 「Tests」と「Configurations」タブから構成される設定画面が表示されます。

「Tests」タブはスキーム設定画面におけるテスト一覧と同じような見た目をしており、 同様に「実行するテスト対象」を選択する画面となっています。

「Configurations」タブは、 テスト実行時における設定(以降「コンフィギュレーション」と表記)を設定する画面となっており、 前述した言語設定(日本語、英語など)にくわえカバレッジ取得の有無なども設定可能です。

コンフィギュレーションは1つの.xctestplanで複数持てるようになっており、 左側のサイドバーにその一覧が表示されるようになっています。

「Shared Settings」と表示されているのは共通設定、 その下に表示されたものが個別のコンフィギュレーション(スクリーンショット中では「Configuration 1」)となっており、 個別のコンフィギュレーションは「Shared Settings」の設定値を継承する仕組みとなっています。

複数の設定を用意して実行する

例として「日本語」と「英語」の2つのコンフィギュレーションを用意してテスト実行する例を紹介します。

まず左下の「+」ボタンから「Japanese」と「English」というコンフィギュレーションを作成します。 そして、それぞれの「Application Language」の項目を「Japanese」と「English」に設定します。

ここまで設定しておくと、 テストナビゲータのコンテキストメニューから実行するコンフィギュレーションを選べるようになります。 ここでは「All Configurations」を選択して、 すべてのコンフィギュレーションでテストを実行してみます。

レポートナビゲータでテストの実行結果を確認すると、 「Japanese」と「English」の2つのコンフィギュレーションで実行されていることが分かります。

当然ながら各コンフィギュレーションごとにテスト結果がわかるようになっており、 以下のアイコンからコンフィギュレーションで絞り込むことも出来ます。

コマンドラインから利用する

xcodebuildコマンドにもTest Plans用のオプションが追加されたので、ここで紹介したいと思います。

プロジェクトに用意されたTest Plansの一覧を表示するには-showTestPlansオプションが利用できます。

$ xcodebuild \
    -project ./TestPlanSample.xcodeproj \
    -scheme 'TestPlanSample' -showTestPlans

### 出力内容:
Test plans associated with the scheme "TestPlanSample":
        AllLanguage
        UITestJapanese

テスト実行時に利用するTest Plansを指定する場合は-testPlanを利用します。

$ xcodebuild test \
    -project ./TestPlanSample.xcodeproj \
    -scheme 'TestPlanSample' \
    -destination 'platform=iOS Simulator,name=iPhone Xs' \
#    -testPlan 'AllLanguage' # テストプラン「AllLanguage」を実行

なお、-testPlanを指定しないで実行した場合はスキーム設定でデフォルトになっているものが利用されます。

ちなみにデフォルトではすべてのコンフィギュレーションが実行対象となりますが、 次のオプションを利用することで限定することもできます。

  • -only-test-configuration:指定したものだけを実行
  • -skip-test-configuration:指定したものを除外して実行
$ xcodebuild test \
...
    -testPlan 'AllLanguage' \
#    -only-test-configuration 'English' # コンフィギュレーション「English」のみを実行

Result Bundles

Result Bundles(.xcresult)は、 今まで独立していた以下のようなテスト結果を1つにパッケージするものです。

  • ビルドログ
  • テストレポート
  • コードカバレッジ
  • テスト添付ファイルなど

またxcresulttoolというコマンドも追加され、 コマンドラインからも.xcresultファイルから情報を取得でき、 JSONスキーマで仕様が明確化されたJSONでの取得も可能になっています。

こうした変更により、テスト結果リソースの扱いがシンプルに行えるようになりました。

どのように変化したか?

Xcode 10.2までは以下のスクリーンショットのように、 それぞれの結果リソースが独立したファイルとして出力されていました。

それがXcode 11からは以下のキャプチャのように、 1つの.xcresultファイルとして集約されるようになりました。

単に集約されただけではなくデータサイズについても考慮されており、 WWDC19のセションの発表によると、 これまでに比べて「最大で4倍小さいサイズ」のフォーマットとなっているようです。

この.xcresultファイルはXcodeで開いて確認できるのに加え、 前述したようにxcresulttoolという専用のコマンドを利用して、 中身の情報を取得できるようになっています。

コマンドラインで出力先を指定する

xcodebuildコマンドで-resultBundlePathオプションを利用すると、 指定したパスに.xcresultファイルを出力することが出来ます。

$ xcodebuild test -project UnitTest.xcodeproj \
    -scheme UnitTest \
    -destination 'platform=iOS Simulator,name=iPhone Xs' \
    -resultBundlePath test_output/ResultBundle.xcresult

なお、このオプションを指定しなかった場合、 プロジェクトで設定されたDerivedDataフォルダ以下に出力されます。

DerivedData/{schemeName}/Logs/Test/Test-{schemeName}-{buildNo}.xcresult

Xcodeで開いて中身を確認する

Finderからダブルクリックするか、 「Xcodeメニュー>File>Open」から.xcresultファイルを選択して開くことが出来ます。

すると以下のキャプチャのように、テスト結果をXcode上から閲覧できます。

xcresulttoolの使い方

前述したようにxcresulttoolを利用すると、中身のデータを取得することが出来ます。

以下は、JSONフォーマットとして情報を取得する例です。

$ xcrun xcresulttool get \
  --path ResultBundle.xcresult \
  --format json

出力されるJSONを抜粋すると以下のようになっています。

  "issues" : {
    "_type" : {
      "_name" : "ResultIssueSummaries"
    },
    "testFailureSummaries" : {
      "_type" : {
        "_name" : "Array"
      },
      "_values" : [
        {
          "_type" : {
            "_name" : "TestFailureIssueSummary",
            "_supertype" : {
              "_name" : "IssueSummary"
            }
          },
...
          "issueType" : {
            "_type" : {
              "_name" : "String"
            },
            "_value" : "Uncategorized"
          },
          "message" : {
            "_type" : {
              "_name" : "String"
            },
            "_value" : "XCTAssertTrue failed - \"hello\" is not empty"
          },
          "producingTarget" : {
            "_type" : {
              "_name" : "String"
            },
            "_value" : "UnitTestTests"
          },
          "testCaseName" : {
            "_type" : {
              "_name" : "String"
            },
            "_value" : "OriginalAssertionGoodTests.testAssertEmpty()"
          }
...

各項目ついての説明は割愛しますが、 かなり詳細にデータを取得できるのが分かります。

なお、JSONスキーマの取得は以下のコマンドで行えます。

$ xcrun xcresulttool formatDescription get

同様に出力結果を抜粋すると以下のようになります。

Name: Xcode Result Types
Version: 3.21
Signature: PVlziQ9KewI=
Types:
  - ActionAbstractTestSummary
    * Kind: object
    * Properties:
      + name: String?
  - ActionDeviceRecord
    * Kind: object
    * Properties:
      + name: String
      + isConcreteDevice: Bool
      + operatingSystemVersion: String
      + operatingSystemVersionWithBuildNumber: String
      + nativeArchitecture: String
      + modelName: String
      + modelCode: String
...

その他コマンドの詳細についてはman xcresulttoolから確認できます。

コードカバレッジ結果の取得

以前からxccovコマンドによってカバレッジレポートの中身を参照できました。 ある意味当然といえますが、 今回から.xcresultファイル形式もサポートされました。

.xcresultファイルからカバレッジ確認する際は、以下のコマンドで行えます。

$ xcrun xccov view --report ResultBundle.xcresult

参考まで出力結果の画面キャプチャを貼っておきます。

XCTest - XCTUnwrap()

本記事の最後として、 XCTestに追加されたXCTUnwrap()というAPIについて解説します。

XCTUnwrap()は、 オプショナル型に対してXCTAssertNotNilのアサーションと、 アンラップ処理を同時に行うAPIです。

具体的なコード例を挙げると、 これまでは以下のようなコードを書く必要がありました。

func testExample() {
    
    let string: String? = "Hello"
    
    // `nil`でないことを検証+アンラップ
    guard let s = string else { XCTAssertNotNil(string); return }
    
    // 検証
    XCTAssertEqual(s, "Hello")
}

それがXCTUnwrap()を利用すると以下のように書けるようになります。

func testExample() throws { // `throws`キーワードが必要な点に注意
    
    let string: String? = "Hello"
    
    // `nil`でないことを検証+アンラップ
    let s = try XCTUnwrap(string)
    
    // 検証
    XCTAssertEqual(s, "Hello")
}

なお、guard letではなく強制アンラップ(!)を利用すれば、 XCTUnwrap()を利用せずともシンプルに書けるのではないか、 と感じた方もいるかもしれません。

強制アンラップは記述こそ簡単ですが、 アンラップの失敗によりテストケースが失敗した場合、 「どの箇所で失敗したのか」という情報が失われ原因調査にコストがかかるという欠点があります。

なので、オプショナル型の中身を取り出して検証したい場合は、 今回追加されたXCTUnwrap()を利用していくとよいでしょう。

なお、T型とOptional<T>型はXCTAssertEqual()などの関数で比較可能なので、 中身を取り出す必要がない場合には以下のように記述できます。

let string: String? = nil
XCTAssertEqual(string, "Hello")

おわりに

本記事では、 WWDC19で発表されたXcode 11における自動テスト周りの変更点として、 「Test Plans」「Result Bundle」「XCTUnwrap()」について紹介しました。

今年のWWDC19では「SwiftUI」や「Catalyst」が大きく注目を集めていましたが、 開発における基盤を支える機能もしっかり進化していたことが分かります。 特に本記事で取り上げたような自動テスト周りの機能は、 ここ数年でかなり成熟してきたようにも感じます。

本記事で主要な機能については解説しましたが、 より詳細なテクニック・組み合わせ方法については、 まだ十分にナレッジを得られていない部分もあると感じています。

より多くの方がiOSアプリ開発コミュニティに対して、 様々な知見を公開してくださると嬉しい限りです。

仲間を募集中

お約束ではありますが、 SWETチームでは自動テストやCI/CDを活用する仕事に興味を持った方を募集しています。

以下のエントリフォームから応募できるほか、 私や他のSWETメンバーに声をかけて頂くかたちでも大丈夫です。

是非ともお気軽にご連絡ください!