Androidアプリのログが正しく送信されていることを担保する

こんにちは。NewsPicks Androidアプリエンジニアの sefwgweo です。
今回は[こちらの記事](https://tech.uzabase.com/entry/2021/05/21/094101)で書かれているログまわりについて、掲題の通りAndroidの事例として紹介します。

なお、概要等についてはリンクの記事に全て詳細に書いてあるのである程度割愛し、本記事ではAndroidで工夫した部分に焦点を当てていきます。

## ログE2Eテストの大まかな仕組み

AndroidでUIテストということでJUNIT４系とEspresso3系を用いて実装しました。

テストコードにはテスト対象のログが送信されるときの画面操作を実装しています。例えば、ニュースフィードで記事を表示する、記事をタップする、といった操作です。

ログ送信のエンドポイントはスタブ化していて、テスト時のログが本番環境に送信されないようにしています。さらに、ログリクエストの内容をあとで検証できるように、スタブ化する過程でリクエスト内容をファイルに一時保存しています。

## ログE2Eテストの実装の詳細

### 1. ログ送信エンドポイントのスタブ化とリクエストの保存
スタブ化には[okhttp3系](https://github.com/square/okhttp)を使用しています

都度スタブ化が必要な場合に `okhttp3.Interceptor`を継承したXXInterceptorを実装していきます。

以下はログ送信をスタブ化している実装部分です。

```kotlin
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データをテスト時に受け取り、これをスタブのレスポンスとして設定します

```kotlin
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に変換したものをファイルに保存します
②：検証時は保存されたファイルから１行ずつURLを読み込み、オブジェクトのListに変換します
③：該当テストの期待値ファイルをJsonで読み込み、オブジェクトのListに変換します
④：②と③を比較してテストします

### 4. Firebase Test Labでログテストを実行する

iOS同様Bitriseではまだβ版ではありますが、Firebase Test Labでテストを実行する仕組みが用意されていて、Androidでもこちらを利用しています。
特筆事項としては、ローカルではうまく動いているのにFirebase上ではうまく動かない事が多く、そのほとんどがおそらく仮想マシン側の問題と思われ、
回避方法としてはそこそこのwaitを入れています。
そのため、同じ処理をローカルで実行するとかなりの時間ロスになっていました（Firebase上でまともに動かすのに２０秒程度waitが必要でもローカルでは５秒程度で十分だったりする）
ということで、Androidではローカルで実行する専用のフレーバーを作ってwaitをローカルとFirebaseとで使い分けられるようにして問題を回避しました。
また、テストケースがそろそろ３桁になりそうな現在では１テストにケースを増やしすぎるとOOMでこけてしまうようなので、これらは１テストを４０分〜６０分で終わる範囲でクラスを分割し
対応しました。
その結果、現在８クラスで運用中です。こちらの運用周りに関しては機会があれば別で書きたいと思います。

### おわりに
UIもそうですが、OSが異なると同じような仕組みを作ろうとしてもそこそこ違いが出るのが面白くもあり、辛くもあるなぁと感じました。

明日は25日目@enkさんです。お楽しみに。