Android
test
AndroidStudio

AndroidでContextが必要な機能のテストコードを簡単に書こうとしてとても時間がかかった話

はじめに

事の始まりはres/rawに置いたJsonファイルの読み込み処理が正しくできているか?というテストコードを書きたかったことです。
エミュレータを起動していちいち確認するのが嫌だったのでUnitテストを書きたかったのですが、すぐ書けるだろと慢心していたら完全に詰まったのでそこで調べたことをまとめようと思います。
初歩的で自分の無知をさらけ出す羽目になるのですが、今後同じような境遇の方の助けになればと思います。。。

対象読者

  • Androidのテスト書こうとググってコピペしたけどAndroidJUnit4.classなんて使えない
  • Androidのテスト書こうとググってコピペしたけどInstrumentationRegistryが使えない
  • AndroidTestCaseを継承するのだ!とあるけどDeprecatedですどうすれば・・
  • Contextが必要なテストコードを楽にさっさと書きたい

作業環境

  • Android Studio3.1
  • com.android.tools.build:gradle:3.0.1
  • junit:4.12
  • com.android.support.test:runner:1.0.1

結論

UnitテストでContext使う場合はRobolectricが一番楽だと思います。
以下、サンプルです。

build.gradle
  android {
    ...
    // AndroidStudio3を使用している場合はこのテストオプションが必要
    // 詳細は http://robolectric.org/getting-started/
    testOptions {
        unitTests {
            includeAndroidResources = true
        }
    }
  }
  dependencies {
    // 2018年1月27日時点の最新版
    testImplementation "org.robolectric:robolectric:3.6.1"
  }

build.gradleを設定したら対象のテストクラスで以下のようにしてContextを取得できます。

  // 1. RobolectricTestRunnerを指定する
  @RunWith(RobolectricTestRunner.class)
  public class HogeTest() {

    @Test
    public void read() {
      // 2. contextを取得する
      Context context = RuntimeEnvironment.application;
      // 以降はcontext.getResourceやcontext.getStringなどでres情報にアクセスできる
    }
  }

この記事の本題

長い上にテストに精通した方なら「はあ?そんなことも知らねーのかよ」となる内容なので暖かい目で読んでください・・・

そもそもなぜ苦労したのか

最初、この作業に取りかかるとき「Android test context」といったワードでググり、src/test配下にテストクラスを作ろうとしていました。
小難しい方法は嫌だったので、Mock作ってwhengetResourceが呼ばれた時ウンヌンみたいなのは辛いから、とにかくContextさえ取れて一番手っ取り早い方法がいい、といった具合で調べました。
そうすると以下のようなことが起こります。

  1. @RunWithAndroidJUnit4.classと指定しろと色々なサイトに書いてあるのに、そもそもJUnit4しか選べない。
  2. ContextInstrumentationRegistryで取得可能とあるけどInstrumentationRegistryなんてクラスはないしimportできない
  3. テストはAndroidTestCaseを継承しgetContextすればOK!とあるけどこのクラスはすでにDeprecatedになっていてどうすれば・・ stack overflowでAndroidJUnit4.classは廃止されてJUnit4.classを使うんだよとかありましたが、developer.android.comでは普通にAndroidJUnit4使っている例があるのでそんなはずはないと思いつつも、でもbuild.gradleで色々試しても上手くいきませんでした。

解決に辿り着いたきっかけ

しばらく考えて、ふと気になってandroidTestExampleInstrumentedTest.java(最初に自動生成されるコード)を開いたらAndroidJUnit4.classを指定しInstrumentationRegistryを使ってContextを取得していました。

この時点で「ああ、そうか・・なんか色々理解してなさそうだな」と私の中で色々と腑に落ちることになりました。

Local Unit TestとInstrumented Tests

再度、この2つのテストについておさらいすることにしました。
developer.android.comのページにてLocal Unit TestInstrumented testsについて説明されています。

Local unit testssrc/testInstrumented testssrc/androidTestとそれぞれディレクトリが分かれており、UnitテストはJVM上で実行され、Instrmentedテストはハードウェアデバイスやエミュレータ上でそれぞれ実行されます。そして、InstrmentedテストではContextなどUIに関するアクセスを提供するInstrumentation APIsが使えると書かれています。
ではInstrumentation APIsはどうやったら使えるのかというと、テスト用のAndroidManifest.xmlに指定する必要があり、それはGradleがビルド時に自動生成してくれるようで、我々利用者側で意識する必要はありません。src/androidTestで作成したテストクラスでのみInstrumentation APIsを使えるのだと思われます。
また、最初に詰まったContextを取得するために必要であろうAndroidJUnit4.classInstrumentationRegistryandroid.support.testパッケージで定義されており、これらはUnitテストのディレクトリ(src/test)ではimportできません。
(実際にはimportだけなら無理矢理できましたので次節で説明します。)
かなり恥ずかしい話ですが、私はandroidTestにはUIテストを、testにはUnitテストを書くというだいぶ大雑把な理解しかしていませんでした。
そもそもAndroidJUnit4InstrumentationRegistrytestパッケージで使えないのは当たり前でした。

なぜLocal Unit TestではAndroidJUnit4やInstrumentationRegistryが使えない?

それぞれAndroidJUnit4android.support.test.runnerInstrumentationRegistryandroid.support.testのクラスです。
これらのクラスがsrc/test配下に作成したUnitテストクラスでimportできない理由は、依存先が異なるためです。
build.gradledependenciesでは、testImplementation(またはtestCompile)androidTestImplementation(またはandroidTestCompile)の2種類が指定できます。この指定の違いは以下の通りです。

  • testImplementation: Local unit testsに依存させる指定で、src/testで作成したテストクラスで使用可能
  • androidTestImplementation: Instrmented Testsに依存させる指定で、src/androidTestで作成したテストクラスで使用可能

サンプルのbuild.gradleを例に、どの指定がどこに影響するか示します。

build.gradle
  dependencies {
    ...

    // 以下の指定はUnitテストに依存。src/testディレクトリで使用可能。ただ、junit4は特殊でInstrmentedテストでも利用可能
    testImplementation 'junit:junit:4.12'
    testImplementation "org.robolectric:robolectric:3.6.1"

    // 以下の指定はInstrmentedテストに依存。src/androidTestディレクトリで使用可能
    androidTestImplementation 'com.android.support.test:runner:1.0.1'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
  }

ということは、例えばandroidTestImplementation 'com.android.support.test:runner:1.0.1'
testImplementation 'com.android.support.test:runner:1.0.1'
と書き換えればUnitテストでもAndroidJUnit4InstrumentationRegistryがimportできる理屈になるはずです。実際、書き換えたらimportできました。
ただ、importできるだけでテスト実行時にエラーが出ました。これはInstrumentation APIsがUnitテストでは使えないためだと思われます。
基本的にtest:runnerInstrumentation APIsとセットで使用する必要があるという理解であっていると思います。そもそもこんな指定ができないよう、わざわざUnitテストとInstrmentedテストを区別しているのでこの結果は当たり前でした。
公式ではありませんが、stack overflowに同じような疑問を持つ方がいて回答が参考になりました。
https://stackoverflow.com/questions/37821148/why-cannot-i-import-androidjunit4-and-activitytestrule-into-my-unit-test-class/37840321

じゃあUnitテストでContext使うには?

ここまでの話を理解すれば簡単です。私はAndroidのテスティングフレームワークが色々あって面倒臭いなと思っていました。しかし、こうした事情が理解できれば色々ある理由も分かるしどれを使えばいいかも見えてきます。今回のケースに一番当てはまるのはRobolectricかと思います。
ここで最初に書いたまとめに辿り着きます。ほんの少しの追記でContextをUnitテストで扱うことができるようになっていい感じです。
ただし、今回はAndroidStudio3系でかつres/rawのファイル読込テストをしたかったため、そのまま実行しようとしたら android.content.res.Resources$NotFoundExceptionが発生しました。
これはAndroidStudio3系ではデフォルトでAAPT2がONになっていることが原因で、リソースファイルをテストで扱う場合は注意が必要でした。
このエラーは、まとめに書いたオプションを指定することで解消します。
なお、Droidkaigi2017のカンファレンスアプリでも同様の方法でContextを取得していたので、最初からそっちを参考にしていればもっと早く解決できた気がします。。
ただ、自分があやふやな理解であることを自覚できたため、まとまった時間をとって改めて調査できたのは良かったかなと思います。