カスタムViewが想定通りに描画されているかテストする
カスタムViewを作って、しかもそれがCanvasを使って描画するようなものだった場合、どうやって動作確認をしていますか?
私はこれまで実機で動かして、目視で確認していました。Viewの見た目なので目視で確認するしかないんですけどね。それを手動でやっていました。
しかしつい先日、手動での確認が難しい案件に出くわしました。それは端末のセンサーの値を読み取って、その値にあわせてカスタムViewの描画が変わるようなものでした。これは手動で確認したくとも難しいです。
例えば心拍数を元に描画が変わるカスタムViewを想像してみてください。心拍数が120を超えたら特殊な表示を行う仕様だと思ってください。実機でそれを確認しようと思ったら、心拍数を上げるべく毎回運動しなきゃいけない、なんてことになるわけです。
そういったViewの描画、見た目の確認がしたい。こういうの、みんなどうやってテストしているのだろう。それが今回の出発点です。
サンプルプロジェクトをGitHubに置いてみたので良かったら見てみてください。というよりコードの解説はこの記事では一切ありませんので、GitHubでみてください。
やり方書かないのもあれなので、追記しました。
TextViewの周りを線でデコレーションするカスタムViewがテスト対象です。どこを描画するかを指定してinvalidate()すると、TextViewの周りに線が描画されます。onDraw
メソッドをオーバーライドして、Canvasを使って線を描いています。
今回はこの描画がちゃんとできるかを確認する、というそんなテストです。
Viewの描画を確認したいわけですから、ユニットテストでは確認できません。
そこでまず思いついたのが、スクリーンショットを撮って、その画像で確認できたらいいんじゃないかというものでした。以前にEspresso+Spoonで自動的にスクリーンショットを撮るテストの話を見たのを覚えていたので、これを使えばいけそうと考えました。
しかしSpoonを使ってスクショを撮るには、WRITE_EXTERNAL_STORAGEパーミッションが必要になります。プロダクト側で必要なら問題ありませんが、そうでない場合はテストのためだけに不要なパーミッションを追加することになります。できればそれは避けたい。
また、スクショはActivityを起動してそれを撮影することになるわけですが、実際に対象のViewを表示するActivityがテストに適した作りになっているとは限りません。
例えばこのサンプルプロジェクトでも、MainActivityを使ってテストできなくもありません。Espressoを使ってボタンを押すようにすれば、カスタムViewの描画は切り替わります。しかしこのMainActivityの仕様だと、カスタムViewの上と下に線を描画した状態をテストできません。
つまり、実際に使うActivityとは別にテストのためだけのActivityが欲しいわけです。
ではそんなActivityをプロダクションに混ぜるのかという話になりますが、それも避けたい。
そこでテスト用のプロダクトフレーバーを作成することでこれを回避しました。これもあまりスマートなやり方ではなく、できれば避けたかったのですが仕方ありません。
debugビルドにだけテスト用のパーミッション、Activityを含めるという方法もなくはないのですが、プロダクトフレーバーで切り分けてしまったほうが潔いかなと思ったのです。
テスト用のAndroidManifestとActivityさえ用意できれば、後は簡単です。
ちなみに私は最初、androidTest配下にテスト用のActivityを追加して、それ経由でテストすればいいんじゃないかと考えました。しかしそれはうまくいきません。
なぜなら、androidTestに配置したコードはテスト用のAPKにコンパイルされるからです。
私は今までずっと勘違いしていました。androidTestに書いたテストを実行したら、mainに配置してるテスト対象コードにテストコードを追加したAPKが作成されて、それでテストが実行されてるんだと思ってました。どうもそうではなくて、普通のAPKを単にテスト用APKで外部から操作してただけなんですね。
https://stackoverflow.com/questions/27826935/android-test-only-permissions-with-gradle
まずproductFlavorを追加します。サンプルでは普段使うやつをDefault、Viewのテスト用のものをUiTestとしました。ここではUiTestを追加するとして書いていますので、適宜読み替えてください。
まずapp/build.gradleにproductFlavorの設定を追加します。applicationIdSuffixはお好みで。
android {
productFlavors {
Default {
}
UiTest {
applicationIdSuffix ".uiTest"
}
}
// そのままだとUiTestReleaseもbuildVariantに追加されてしまうので、それに対処
android.variantFilter { variant ->
if(variant.buildType.name.equals('release')
&& variant.getFlavors().get(0).name.equals('UiTest')) {
variant.setIgnore(true);
}
}
}
プロジェクトルートのbuild.gradleに追記。
classpath 'com.stanfy.spoon:spoon-gradle-plugin:1.2.2'
app/build.gradleに追記。
apply plugin: 'spoon'
android {
defaultConfig {
// 追加しないと多分テストがうまく走ってくれないと思います。
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
}
dependencies {
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
androidTestCompile('com.android.support.test:runner:0.5', {
exclude group: 'com.android.support', module: 'support-annotations'
})
androidTestCompile 'com.squareup.spoon:spoon-client:1.6.4'
}
プロジェクトツールウィンドウのスコープをProjectに変更して、手動でディレクトリを作成します。(何か他にいい方法知ってれば教えてください)
<project root>
+-app
+-src
+-androidUiTest
| +-java
| +-<your package>
| +-Viewの描画確認とスクショを撮るコードをここに配置
+-UiTest
+- AndroidManifest.xml
+-java
| +-<your package>
| +-Viewの描画確認のためのActivityを配置
+-res // layout.xmlが必要なら作る
AndroidManifest.xmlに書くこと
- `WRITE_EXTERNAL_STORAGE`の追加
- 追加したActivityの宣言
サンプルコードを見てもらえば分かりますが、ビルド時にmainにおいてあるAndroidManifest.xmlとマージしてくれるので、UiTestで必要な分だけ書けばOKです。
別にアサーションは必要ないし、Espresso使っていると言ってもViewの操作をするわけでもないので(それをしなくていいようにテスト用のActivityを用意している)、テストコード書くのは超簡単なはず。
- buildVariantを`UiTestDebug`に変更
- ターミナルで`./gradlew :app:assembleUiTestDebug`
- ターミナルで`./gradlew :app:assembleUiTestDebugAndroidTest`
- ターミナルで`./gradlew :app:spoonUiTestDebugAndroidTest`
- app/build/spoon/UiTestにレポートが生成される
- index.htmlをブラウザで開く