Help us understand the problem. What is going on with this article?

AndroidのUnitTestのJUnit4移行とカバレッジ計測(+Mockitoの依存関係解決)

More than 3 years have passed since last update.

背景

AndroidにてUnitTestを書く際には、ApplicationTestCaseとかActivityInstrumentationTestCase2とかServiceTestCaseなどを使っていたと思うが、Android 7からこれらは非推奨になったので、書き換える必要がある。
また、従来はJUnit3しかサポートしていなかったが、新方式ではJUnit4が使えるようになったので、値の検査がやりやすくなっている。

本記事のスコープ

Android公式ののBuilding Effective Unit Testsには、

  • Local Unit Test
  • Instrumented Unit Test

の二つが紹介されているが、この記事ではInstrumented Unit Testについて記述する。ついでに、カバレッジ計測をしたいという人も多そうなのでその設定についても触れる。

Local Unit Testはいずれ書くかもしれない。

従来方式からの移行

セットアップ

基本的にはBuilding Instrumented Unit Testsに書いてあるが、文章量多いので以下にまとめておく。

dependenciesの追加

build.gradleのdependenciesブロックに以下を追加。

build.gradle
dependencies {
    androidTestCompile 'com.android.support:support-annotations:24.0.0'
    androidTestCompile 'com.android.support.test:runner:0.5'
    androidTestCompile 'com.android.support.test:rules:0.5'
    // 以下任意
    androidTestCompile 'org.hamcrest:hamcrest-library:1.3'
}

com.android.support:support-annotations:24.0.0等は、現在のbuild.gradleに指定しているbuildToolsVersionによって変える必要があるので、Android Studio上でエラーが出たら書き換える。

androidTestCompile 'org.hamcrest:hamcrest-library:1.3'
も書いておくと、assertThatによる検証で便利なメソッドが追加される。

AndroidJUnitRunnerの指定

build.gradleのdefaultConfigブロックに以下を追記。

build.gradle
android {
    defaultConfig {
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
}

@RunWithの記述

テストクラスの定義前に、@RunWith(AndroidJUnit4.class)を記述する。ApplicationTestCaseとか継承しているなら、継承をやめて、コンストラクタも消す。

import android.support.test.runner.AndroidJUnit4;

@RunWith(AndroidJUnit4.class)
public class LogHistoryAndroidUnitTest {
}

setUp(), tearDown()の置き換え

JUnit3では、

protected void setUp() {}
protected void tearDown() {}

の二つのメソッドを使って、各テストケースの実行前、実行後に行う提携処理を定義していた。
JUnit4では、メソッド名に命名規則はなく、アノテーションで指定する。

@Before
public void beforeExecute() {}
@After
public void afterExecute() {}

これらのアノテーションを指定するメソッドはpublic methodでないといけない。ゆえに、アノテーションだけでなく、メソッドの可視性もpublicに変更する必要がある。

// メソッドはpublicにすること!

@Before
protected void setUp() {}
@After
protected void tearDown() {}

上記はエラーとなる。

テストケースの書き換え

JUnit3では、メソッド名の接頭辞がtestとなっているメソッドをテストメソッドと認識していた。

public void test_CheckValue() {
    assertEquals(hoge, 1);
}

JUnit4では、アノテーションを使って指定する。

@Test
public void checkValue() {
    assertEquals(hoge, 1);
}

典型パターンごとの書き換え方法

ActivityInstrumentationTestCase2の置き換え

ActivityTestRuleを使う。以下のようにすればActivityが取得できるので、あとは良しなに。

private Activity mActivity;

@Rule
public final ActivityTestRule<HogeActivity> mActivityTestRule = new ActivityTestRule<>(HogeActivity.class);

@Before
public void beforeEachTest() {
    mActivity = mActivityTestRule.getActivity();
}

ServiceTestCaseの置き換え

ServiceTestRuleを使う。以下のようにすればServiceを起動できるので、あとは良しなに。bindService()もあるので、bindしたければそちらを使う。

@Rule
public final ServiceTestRule mServiceTestRule = new ServiceTestRule();

@Before
public void beforeEachTest() {
     mServiceRule.startService(
         new Intent(InstrumentationRegistry.getTargetContext(), HogeService.class));
}

Contextの取得

InstrumentationRegistrygetContext()getTargetContext()を使う。getContext()は、テスト実施側のContextの取得を行うので、テスト対象のContextが欲しい場合はgetTargetContext()を使うこと。
ActivityInstrumentationTestCase2.getContext()だとInstrumentationRegistryにおけるgetTargetContext()相当のものが取れていたはず。

@UiThreadTestの置き換え

以下が使えるが、

  • 新しい方の@UiThreadTest
  • UiThreadTestRule.runOnUiThread(Runnable)
  • ActivityTestRule.runOnUiThread(Runnable)
  • InstrumentationRegistry.getInstrumentation().runOnMainSync(Runnable)

一番上の@UiThreadTestは特殊な副作用があるのでお勧めしない。二つ目のUiThreadTestRule.runOnUiThread(Runnable)を使おう。

使い方は簡単で、UI Threadで実行したい処理について、

UiThreadTestRule.runOnUiThread(new Runnable() {
    // UI Threadで実行したい処理
});

とする。

以下に@UiThreadTestの副作用を書いておくが、長いので気になる人以外は読まなくていい。

@UiThreadTestの副作用

android.support.test.annotation.UiThreadTestで従来の@UiThreadTestっぽいことができる。ただし、UiThreadTestRuleやそれを継承したクラス(ActivityTestRuleなど)をnewしておかないと効果がない。

ただし、単純なリプレイスはできなくて、@UiThreadTestを指定してかつUiThreadTestRuleをnewしている場合は、@UiThreadTestを指定しているテストケースの実行時には@Before@AfterもUiThreadで動作するようになる。@UiThreadTestを指定していないテストケースの実行時には@Before@After@Testはandroid.support.test.runner.AndroidJUnitRunnerで動作する。

この挙動は案外厄介で、例えば、Serviceのテストをする際に、@Beforeで該当サービスとbindしておきたいケースがあり、以下のようなコードを書いたとする。

@Before
public void setup() {
  mServiceTestRule.bindService(intent, new ServiceConnection() {
      @Override
      public void onServiceConnected(ComponentName componentName, IBinder binder) {
          mService = ((SomeService.LocalBinder) binder).getService();
      }
  }
}

@UiThreadTestを指定していなければ、setupメソッドはAndroidJUnitRunnerのThreadで動作するが、ServiceはUI Threadで動作するため、setUpメソッドでbindService後に適宜待っていればonServiceConnectedが呼び出される。

しかし、@UiThreadTest指定すると、setupメソッドがUI Threadで動作するようになるため、setupメソッドがThreadを手放さない限りServiceは開始できない(警告: サービスは、そのホスト プロセスのメインスレッドで実行します。)。
しかし、setupメソッドを抜けてしまうと、ただちにテストメソッドが走りだしてしまうため、Serviceの接続を待てないというジレンマが生じる。
だからUiThreadTestに以下のような記述があるんだなぁ。

Note, due to current JUnit limitation, methods annotated with Before and After will also be executed on the UI Thread. Consider using runOnUiThread(Runnable) if this is an issue.

そんなめんどくさいこと考えたくなければ、UiThreadTestRule.runOnUiThread(Runnable)を使うのが良い。

カバレッジの計測

セットアップ

app/build.gradleの適当なビルドタイプに、testCoverageEnabled trueを指定する。

build.gradle
android {
    ....
    buildTypes {
        ....
        debug {
            testCoverageEnabled true
        }
    }
}

実行

createDebugCoverageReportタスクを実行する(flavor定義している場合はcreate${FLAVOR_NAME}DebugCoverageReport)
一つでもFailするテストがあるとレポートが作成されないので注意
結果ファイルは、
app/build/outputs/reports/coverage/debug
に出力される。(flavor定義しているとちょっとパス変わる)

assert例とテストの制御

assert文のJUnit4化

assertEquals(expected, actual)のままでもテストは通るが、せっかくだしJUnit4スタイルにしたいという場合は、assertThat(actual, is(expected))に変更すればよい(expectedとactualの出現順序が変わっているので注意)。

JUnit4化の一番のメリットはassert文にて柔軟な検査ができるようになったことだと思うので、落ち着いたら下記の記事を読んでからテストケースを書くことをお勧めする。
HamcrestのMatchersに定義されているメソッドの使い方メモ

テスト実行のタイムアウト設定

テスト実行するとデッドロックをしているのかうんともすんともいわなくなることがあるが、以下のようにタイムアウト時間を設定することで自動的に失敗と判定することができる。

@Test(timeout=1000)
public void testWithTimeout() {
    // some test
}

exceptionの検査

Exceptionが発生するのが正しいという場合には、以下のように記述することができる。
try-catchでもいいかもしれないけれど。

@Test(expected = IndexOutOfBoundsException.class) 
public void empty() { 
     new ArrayList<Object>().get(0); 
}

テストケースのタイプ分類と実行

テストケースを@SmallTest, @MediumTest, @LargeTestに分類して、実行時にどのタイプを走らせるか指定することができる。
Android の Instrumented Test で指定のサイズのテストだけ実行する (@SmallTest とか@LargeTestとか)

テストが素早く実行できれば、commitごとに自動で実施してデグレしてないかチェックするというようなことができるかもしれない。
しかし、テストケースによっては、httpアクセスや複雑なdb処理を行って、その挙動が壊れていないというようなことをチェックしたい場合もあるだろう。
このような場合は、素早く終わるテストを@SmallTestに分類し、時間がかかるけどユースケースに近いテストを@LargeTestに指定しておく。そして、commit毎では@SmallTestを、ブランチのマージ前には@LargeTestも含めて実施するというようなワークフローにしておくと良いかもしれない。

以下、Googleが提唱しているTestSizeという概念と、クックパッドの例の紹介記事。
Googleが提唱するTestSizeとJava,MavenによるTestSizeの実現方法について

Mockitoの導入

Mockitoを使えば、特定のメソッドの挙動を書き換えることができる。これは非常に便利そうで、多くの人が使おうとしたことがあるだろうし、実際にテストコードを書いたことがある人も多いと思うが、実行時にエラーになってしまってあきらめた人が多くいるはず(私は3回あきらめた)。
やれhamcrestが重複しているとか、dexmaker入れないといけないとか、dexmaker.cachedirを指定しないといけないとか・・・

最近、ついに導入に成功したので以下に書いておく。
Instrumented Test Caseで使いたい場合は、app/build.gradleに以下を指定する。

build.gradle
androidTestCompile 'com.linkedin.dexmaker:dexmaker-mockito:2.2.0'

これだけでdexmakerもMockitoも導入されるので、すぐに使い始められる。しかも導入が難しいMockito2らしい。
Local Unit Testで使いたい場合はandroidTestCompileではなくtestCompileにすればよいはず。

PowerMockはいまだに実行時に落ちていて諦めている。。。まあMockitoの中の人もprivateメソッドを直接テストするなと言っているのでいいのかな。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away