こんにちは。NewsPicks Androidアプリエンジニアの sefwgweo です。
今回はこちらの記事で書かれているログまわりについて、掲題の通りAndroidの事例として紹介します。
なお、概要等についてはリンクの記事に全て詳細に書いてあるのである程度割愛し、本記事ではAndroidで工夫した部分に焦点を当てていきます。
ログE2Eテストの大まかな仕組み
AndroidでUIテストということでJUNIT4系とEspresso3系を用いて実装しました。
テストコードにはテスト対象のログが送信されるときの画面操作を実装しています。例えば、ニュースフィードで記事を表示する、記事をタップする、といった操作です。
ログ送信のエンドポイントはスタブ化していて、テスト時のログが本番環境に送信されないようにしています。さらに、ログリクエストの内容をあとで検証できるように、スタブ化する過程でリクエスト内容をファイルに一時保存しています。
ログE2Eテストの実装の詳細
1. ログ送信エンドポイントのスタブ化とリクエストの保存
スタブ化にはokhttp3系を使用しています
都度スタブ化が必要な場合に okhttp3.Interceptor
を継承したXXInterceptorを実装していきます。
以下はログ送信をスタブ化している実装部分です。
class StubInterceptorForNpLog constructor(
context: Context,
environmentManager: EnvironmentManager
) : Interceptor {
// ①
private val operator = StubOperator(context, environmentManager)
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val url = request.url.toUrl().toString()
// ②
if (operator.shouldExecuteStub(url, StubLogType.getPatterns())) {
StubLogType.generateEventNameIfNeeded(url).let {
if (it.isNotEmpty()) {
// ③
operator.saveLog(operator.generateUrlForPost(request, it, url))
} else {
// ④
operator.saveLog(url)
}
}
}
...
}
}
①:スタブ化を実行するクラスです
②:スタブ化処理を実行可能か判定しています。内部ではUIテスト実行用のフレーバーかつデバッグビルドかつurlがスタブ化必要なものならばtrueが返るようにしています
③POST時は送信パラメータからURLを生成しています(例えばエンドポイントが 'http://example.com/test' で、POSTパラメータが'id=3'のときはhttp://example.com/test?id=3
といった感じです)
④:基本的にはGETでログ送信しているため、引数にurlを渡し、そのまま所定のファイルに保存されます
2. ニュースフィードAPIのスタブ化
ニュースフィードのテスト用のレスポンスのJSONデータをテスト時に受け取り、これをスタブのレスポンスとして設定します
class StubInterceptorForFeed constructor(
private val context: Context,
environmentManager: EnvironmentManager
) : Interceptor {
private val operator = StubOperator(context, environmentManager)
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val url = request.url.toUrl().toString()
if (operator.shouldExecuteStub(url, StubReplaceJsonType.getPatterns())) {
val response = chain.proceed(request)
var code = 200
// ①
val body = try {
operator.loadFromAssets(context, StubReplaceJsonType.getE2eJsonPath(request.url.encodedPath))
} catch (e: FileNotFoundException) {
// File not found
code = 404
e.toString()
}
val responseBody = ResponseBody.create("application/json; charset=utf-8".toMediaType(), body)
// ②
return response.newBuilder().code(code).body(responseBody).build()
} else {
// スタブを使わない場合はそのままリクエストを返す
return chain.proceed(request)
}
}
}
①:スタブ化実行クラス内メソッドであるloadFromAssets
を呼び出し、urlに応じたJsonを返します。なお差し替える用のJsonはiOSとも共用のためAndroidではapp/src/debug/assets/submodule/
配下に置くことでデバッグビルド時以外は参照不可にしています。なお、テストフィクスチャ(期待値結果Json)も同様です
②:返ってきたJsonを元にresponseを構築し、スタブ化したものと差し替えています
3. ログリクエストの検証方法
①:GETでもPOSTでも、ログ送信時の内容を一律GETのURLに変換したものをファイルに保存します
②:検証時は保存されたファイルから1行ずつURLを読み込み、オブジェクトのListに変換します
③:該当テストの期待値ファイルをJsonで読み込み、オブジェクトのListに変換します
④:②と③を比較してテストします
4. Firebase Test Labでログテストを実行する
iOS同様Bitriseではまだβ版ではありますが、Firebase Test Labでテストを実行する仕組みが用意されていて、Androidでもこちらを利用しています。
特筆事項としては、ローカルではうまく動いているのにFirebase上ではうまく動かない事が多く、そのほとんどがおそらく仮想マシン側の問題と思われ、
回避方法としてはそこそこのwaitを入れています。
そのため、同じ処理をローカルで実行するとかなりの時間ロスになっていました(Firebase上でまともに動かすのに20秒程度waitが必要でもローカルでは5秒程度で十分だったりする)
ということで、Androidではローカルで実行する専用のフレーバーを作ってwaitをローカルとFirebaseとで使い分けられるようにして問題を回避しました。
また、テストケースがそろそろ3桁になりそうな現在では1テストにケースを増やしすぎるとOOMでこけてしまうようなので、これらは1テストを40分〜60分で終わる範囲でクラスを分割し
対応しました。
その結果、現在8クラスで運用中です。こちらの運用周りに関しては機会があれば別で書きたいと思います。
おわりに
UIもそうですが、OSが異なると同じような仕組みを作ろうとしてもそこそこ違いが出るのが面白くもあり、辛くもあるなぁと感じました。
明日は25日目@enkさんです。お楽しみに。