マルチモジュールな世の中で、安全なInstrumentationTestを書く

この記事は [RNIアドベントカレンダー2024](https://rni-dev.hatenablog.com/entry/2024/12/13/182659) 2日目の記事です。

皆さんこんにちは、株式会社リサーチ・アンド・イノベーション　Androidエンジニアのねこはみんなかわいいです。

ねこはなぜかわいいのか

https://news.yahoo.co.jp/expert/articles/33f12cefd803a43c6ce549a96e0c0cf1d4f5c7f6


# この記事で言いたいこと
~~猫は皆可愛い~~
InstrumentationTestを書くときには、VisibleForTestingを使うと便利だよ、という内容の記事です。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351850/a8c3a737-de7e-619c-d866-514c4f7eb605.jpeg" width=50%>


# VisibleForTestingとは？
https://developer.android.com/reference/kotlin/androidx/annotation/VisibleForTesting

VisibleForTestingは、テスト用のアノテーションを指します。
「通常アクセスできない（private または protected）場所にあるメンバーやメソッドだけど、テストのためだけにアクセス可能にする」 という意図を明確にするためのものです。

例えばこんな経験は無いでしょうか。


> テストを書くために、このメソッドのスコープを
無駄にpublicやprotected,package privateにしないといけない...
テストさえ無ければprivateのままで良いのに...。


こんな時、 `@VisibleForTesting` を付与するのがおすすめです。

ここからは例を使って解説していきましょう。

# 例 : HogeActivityにあるprivateなメソッドをInstrumentationTestから呼び出したい

HogeActivityには、ある重要な値を返すprivateなメソッド `HogeActivity.getImportantValue()` があります。
このメソッドをInstrumentationTestのコードから呼び出したいのですが、**マルチモジュール環境下ではActivityのテストクラスを `:app` 直下に置く**ため、直接呼び出すことができません。

では、どうすれば良いでしょうか？


:::note alert
getImportantValue()をpublicにするのは、他のクラスやモジュールから不必要にアクセスされる可能性があるため、適切ではありません。
:::

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351850/1e64ed4a-e763-2b80-4f75-6977e0bb2f45.jpeg" width=50%>


まず、以下のようなアクセサーを介して getImportantValue メソッドにアクセス可能にし、テストコードはこのアクセサーを使うことによって、必要な検証を行えるようにします。
このアクセサーに付与するのが `@VisibleForTesting` アノテーションです。

```kotlin

import androidx.annotation.VisibleForTesting

@VisibleForTesting(otherwise = VisibleForTesting.NONE)
object HogeActivityAccessor {
    @JvmStatic
    fun callGetImportantValue(activity: HogeActivity): String {
        return activity.getImportantValue()
    }
}
```

VisibleForTestingを使うと、通常のコードの流れでは触れるべきでないメソッドやフィールド（ここでいうgetImportantValue）をテスト用に例外的に公開する意図を示すことができます。
実際のコード内では、private もしくは protected としてアクセスを制限しつつ、テストコードでは呼び出し可能にするためのセーフガードとして機能します。

## InstrumentationTestの例

``` kotlin

@RunWith(AndroidJUnit4::class)
class HogeActivityInstrumentationTest {

    @get:Rule
    val activityTestRule = ActivityScenarioRule(HogeActivity::class.java)

    @Test
    fun testGetImportantValueUsingAccessor() {
        activityTestRule.scenario.onActivity { activity ->
            // HogeActivityAccessor を利用して getImportantValue を呼び出す
            val result = HogeActivityAccessor.callGetImportantValue(activity)

            // 結果が期待通りかを検証
            assertEquals("expectedValue", result)
        }
    }
}
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351850/cc292199-6b97-8037-f4ec-e83e6cef2b54.jpeg" width=50%>


## otherwise の意味と使い所

### 1. otherwise =  VisibleForTesting.PRIVATE
このアクセサーオブジェクトが **通常は private として扱われるべき**であることを示しています。
テストコード（src/test もしくは src/androidTest に配置されたコード）では public 扱いになりますが、それ以外ではコンパイルエラーとなります。

### 2. otherwise = VisibleForTesting.PROTECTED
アクセスレベルが protected として扱われます。
テスト用コード以外でも、特定のサブクラスやパッケージ内から呼び出したい場合に使うそうです。

### 3. otherwise = VisibleForTesting.NONE
テストコード以外からは一切アクセスできません。
テスト用コードでは引き続き public としてアクセス可能ですが、プロダクションコードでは private と同じ扱いになります。

:::note warn
（補足）VisibleForTesting.PRIVATEとの違い
NONEにすると、**プロダクションコードからは一切アクセスできなくなります。**
PRIVATEの場合は **同じクラス内であれば、プロダクションコードからのアクセスが可能**です。
:::

:point_up:

よって、NONEは**テスト目的以外では絶対に呼び出したくないなど、強い制限をかけたい時に使う**のが良さそうです。

今回はこのアクセサー自体がテストのためだけの存在で、同一パッケージからのむやみな参照も防ぎたいため、NONEを指定しました。

https://developer.android.com/reference/androidx/annotation/VisibleForTesting


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351850/9503b3f0-47e5-adb6-f94a-1ec03fed525c.jpeg" width=50%>


## 実際の動作に影響はないの？

Activity側に用意したgetImportantValue は private なまま、内部ロジックで使用されます。
アクセサーやその内部のメソッドは、通常のアプリ動作中に呼ばれることはありません。

## まとめ
重要なメソッドやクラスに対して不用意な外部からのアクセスを遮断しつつ、適切なInstrumentationTestを書くためには、テスト用のアノテーション `VisibleForTesting` が有効であるということが分かりました。

スコープの範囲をどのくらいにすべきかはケースバイケースですが、VisibleForTestingをつけるクラスの用途と設計に応じて決めるのが良いでしょう。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351850/8f20f5e4-b324-060f-2658-54fa5bf64a2e.jpeg" width=50%>

## エンジニア募集中です！

:::note info
現在、RNIではエンジニアを積極採用中です！（業務委託可）
ご応募の他、
- ちょっと話をしてみたい or 聞いてみたい
- エンジニアのキャリアについて悩んでいる
- 猫に気を取られて内容が頭に入ってこなかった

など、少しでも興味がある方はぜひご連絡ください。

◆会社ホームページ
https://r-n-i.jp/

◆ご応募はこちらより
※カジュアル面談も受付中です。
https://r-n-i.jp/recruit/engineer/

◆どんな人たちが働いてるのか気になる？
https://qiita.com/kotlinObasan/items/b8068e7145175572b671
:::