【Android Studio】JunitでMockの作成【日本語】

Android Studio(JAVA)のJUnitで、Mockを使ったテストをしてみた。

尚、使用したライブラリはMockito(モック伊藤ではなくモキートと読む)とpowermockです。

※本当はMockitoだけでやろうと思ったのですが、そうした場合になぜかMockの注入が出来なかったので、powermockを使いました

※追記。MockitoでMockの注入が出来ない理由が判明。こちらの記事にまとめました

 

他のプログラミングに関する記事はこちら

スポンサーリンク


 

【前提】

・JUnitを使ったAndroidのテストには、エミュまたは実機が実際に動いてテストするinstrumentation test(androidTestフォルダにテスト用クラスがあるやつ)と、コード上のみでテストするunit test(testフォルダにテスト用クラスがあるやつ)がありますが、unit testで行います(チラ裏参照)

・新規プロジェクトを「JUnitTest2」という名称で作成したところから説明します

・各ライブラリは随筆時点での最新版を使ってます

Mockito:org.mockito:mockito-core:3.7.7
powermockito:org.powermock:powermock-module-junit4:2.0.9
powermockito:org.powermock:powermock-api-mockito2:2.0.9

・staticなメソッドをモック化する方法は、【オマケ1】(クリックでそこまで遷移)として載せてます

・privateなメソッドをモック化する方法は、【オマケ2】(クリックでそこまで遷移)として載せてます

 

【チラ裏】

instrumentation testでmock作成は出来ません。

いや、正確に言えばmock作成は出来ますが、mockの注入が恐らく出来ないので、テストとしてあまり意味のないものになります。

というか多分、instrumentation testはUIのテストを行うものなので、mockを作成しなければいけない段階で行うテストじゃないと感じます。

mockだとかstubだとかは以下サイトが図もあるので物凄くわかりやすかったです。

https://recruit.cct-inc.co.jp/tecblog/java/mockito-primer/

以下サイトのmockだとかspyだとかstubのあたりの読んでから本記事を読むと理解が深まると思います。

 

 

【早速実装してみる】

1.build.gradle(Module(app)の方)に、以下「追加」と書いた部分を追記します。

尚、以下「追加」と書いた部分以外にも足りてない記述があったら追加した方がいいかも(Gradleのバージョンによって多分初期値が違うため)

apply plugin: 'com.android.application'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.2"
    defaultConfig {
        applicationId "com.sample.junittest2"
        minSdkVersion 28
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    // 追加(メソッドが失敗したときにnullが返るようにする)
    testOptions{
        unitTests.returnDefaultValues = true
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'

    // 追加(mockito)
    testImplementation 'org.mockito:mockito-core:3.7.7'
    // 追加(powermock)
    testImplementation 'org.powermock:powermock-module-junit4:2.0.9'
    testImplementation 'org.powermock:powermock-api-mockito2:2.0.9'
}

 

ひとつずつ「追加」したところの説明をしていくと、

 

    // 追加(メソッドが失敗したときにnullが返るようにする)
    testOptions{
        unitTests.returnDefaultValues = true
    }

↑テストコードでメソッドに例外エラーが発生した場合にnullを返す設定……らしい

 

    // 追加(mockito)
    testImplementation 'org.mockito:mockito-core:3.7.7'
    // 追加(powermock)
    testImplementation 'org.powermock:powermock-module-junit4:2.0.9'
    testImplementation 'org.powermock:powermock-api-mockito2:2.0.9'

↑それぞれmockitoとpowermockのライブラリを追加。

尚、powermockはandroidTest……つまりはinstrumentation testでは使えないライブラリらしいので注意が必要(そもそもtestImplementationとしてるけど)

 

2.Gradle.buildを更新したら、忘れずにGradleの設定を適用します。

Gradleの設定を適用するには、画面右上にあるゾウさんのアイコンをクリックします。

 

3.最初からある「MainActivity」クラスを以下の通り更新します。

package com.sample.junittest2;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

    public int testInt = 0;
    public int x = 0;
    public int y = 0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void testMethod(){
        Calculator calculator = new Calculator();

        x = 1;
        y = 2;
        testInt = calculator.sum1(x,y);
    }
}

 

追加したのは以下の部分。

testMethodを呼ぶことで、Calculatorクラスのsum1メソッドを利用し、変数を更新します。

    public void testMethod(){
        Calculator calculator = new Calculator();

        x = 1;
        y = 2;
        testInt = calculator.sum1(x,y);
    }

 

4.Calculatorクラスを以下の通り作成します。

sum1メソッドは受け取った引数を合算するメソッドで、publicなメソッドです(staticではない)

package com.sample.junittest2;

public class Calculator {

    // publicなメソッド
    public int sum1(int x, int y){
        return x + y;
    }
}

 

5.testフォルダにテスト用クラスを作成します。

テスト用クラスは基本、[テストするクラス名]Testという名称で作成するっぽい。

今回はMainActivityクラスのテストをしてみるので、MainActivityTestクラスを作成します。

 

6.MainActivityTestクラスの中は以下の通りです。

順を追って細かめに説明していきます。

package com.sample.junittest2;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import static junit.framework.TestCase.assertEquals;
import static org.mockito.ArgumentMatchers.any;

@RunWith(PowerMockRunner.class)
@PrepareForTest({MainActivity.class})
public class MainActivityTest {

    // テスト対象のクラスを生成
    private MainActivity mainActivity;

    // 初期処理
    @Before
    public void setUp(){
        this.mainActivity = new MainActivity();
    }

    // テスト
    @Test
    public void mockTest1() throws Exception{
        // Mockオブジェクト作成
        Calculator mock = Mockito.mock(Calculator.class);

        // Mockオブジェクトのスタブを作成
        int stubResult = 1;
        Mockito.doReturn(stubResult).when(mock).sum1(any(int.class), any(int.class));

        // テスト対象のクラスにMockオブジェクトを注入
        PowerMockito.whenNew(Calculator.class).withNoArguments().thenReturn(mock);

        // テスト対象のメソッド実行
        this.mainActivity.testMethod();

        // 結果取得
        int result = this.mainActivity.testInt;

        // 評価
        assertEquals(stubResult, result);
    }
}

 

以下の記述はお決まり文句です。

@RunWith(PowerMockRunner.class)

PowerMockを使ったテストコードを作成する場合に記述します。

テストクラスを宣言する前(importとかを記述する箇所と同じ階層)に記述するので注意が必要です。

 

以下の記述はテスト対象のクラスを記述します。

@PrepareForTest({MainActivity.class})

複数のクラスをテストする場合(その場合テスト用のクラスをわけるべきと感じますが)以下のように記述することも可能らしいです。

@PrepareForTest({MainActivity.class, xxxxxxx.class, yyyyyyy.class})

 

以下の記述は単純にテスト対象のクラスを生成してるだけです。

    // テスト対象のクラスを生成
    private MainActivity mainActivity;

    // 初期処理
    @Before
    public void setUp(){
        this.mainActivity = new MainActivity();
    }

@Beforeをつけたメソッドは、テストコードが実行される前に実行されます。

 

以下の記述はテストコードを作成しています。

    // テスト
    @Test
    public void mockTest1() throws Exception{

@Testをつけたメソッドがテストコード扱いになります。

 

以下の記述でCalculatorクラスのモックを作成します。

        // Mockオブジェクト作成
        Calculator mock = Mockito.mock(Calculator.class);

 

以下の記述で、Calculatorクラスのモックのメソッドの内容を設定しています。

        // Mockオブジェクトのスタブを作成
        int stubResult = 1;
        Mockito.doReturn(stubResult).when(mock).sum1(any(int.class), any(int.class));

記述方法は以下の通りです。

Mockito.doReturn([返り値]).when([モックオブジェクト]).[メソッド名];

今回のテストの場合、オブジェクトmock……つまりはCalculatorクラスのsum1メソッドが呼ばれた時に、必ずstubResult……つまり1を返すようになります。

any(int.class)というのは、どんな数値が入っても同じということを表しています。

例えば以下のような記述にした場合、Calculatorクラスのsum1メソッドの第1引数に「1」、第2引数に「2」を設定したときにしか、設定した返り値を返してくれません。

Mockito.doReturn(stubResult).when(mock).sum1(1, 2);

渡す値をテストに含めたい場合は上記のように渡す値を設定し、渡す値をテストに含めたくない場合はany()を使います。

 

以下の記述で作成したモックをテスト対象のクラスに注入しています。

        // テスト対象のクラスにMockオブジェクトを注入
        PowerMockito.whenNew(Calculator.class).withNoArguments().thenReturn(mock);

記述方法は以下の通りです。

PowerMockito.whenNew([テスト対象のクラスで呼ばれるクラス]).withNoArguments().thenReturn([置き換えるモックオブジェクト]);

今回のテストの場合、テスト対象のクラス……つまり@PrepareForTestで指定したMainActivityクラスで呼ばれるCalculatorクラスを、mock……つまり前の記述で色々作った内容のクラスに差し替えるということになります。

※Mockitoの場合は@InjectMocksで指定するっぽいけど、自分はなぜかそれで注入することが出来なかった → 理由はこちら

 

【2021/1/29 追記】

今回は作成時に引数が不要(コンストラクタの引数が不要)のクラスを注入するため「withNoArguments()」を使っているが、作成時に引数が必要なクラスを注入する場合は「withArguments(引数)」で指定する必要がある。

例えば、インスタンス化時(コンストラクタ)に、第1引数にString型、第2引数にもString型を指定する必要があるクラスを注入する場合は以下のような感じ。

PowerMockito.whenNew([テスト対象のクラスで呼ばれるクラス]).withArguments(any(String.class), any(String.class)).thenReturn([置き換えるモックオブジェクト]);

 

 

残りの記述はメソッド実行して結果を取得し評価しているだけです(stubResultは1です)

        // テスト対象のメソッド実行
        this.mainActivity.testMethod();

        // 結果取得
        int result = this.mainActivity.testInt;

        // 評価
        assertEquals(stubResult, result);

本来であれば、MainActivityのtestMethodメソッドを実行した場合、testInt……つまりresultの値は必ず3を返します。

が、今回はMainActivityのtestMethodメソッドで呼ばれるCalculatorクラスのsum1メソッドの結果を、モックオブジェクトを使って必ず「1」を返すようにし注入しています。

このテストコードを実行するとpassedするはずなので、正常にモックが作成できていて注入まで出来ていることが確認できるかと思います。

 

スポンサーリンク


 

【オマケ1】

最初に説明した方法はstaticメソッドをモック化することは出来ません。

staticメソッドをモック化する場合、以下のようにします。

前提として、gradle.buildの設定は本記事に書いてある状態にしていることとします。

 

MainActivityクラス

package com.sample.junittest2;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;

import static com.sample.junittest2.Calculator.sum2;

public class MainActivity extends AppCompatActivity {

    public int testInt = 0;
    public int x = 0;
    public int y = 0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void testMethod(){
        Calculator calculator = new Calculator();

        x = 1;
        y = 2;
        testInt = calculator.sum1(x,y);
    }

    public void testMethod2(){
        x = 1;
        y = 2;
        testInt = sum2(x,y);
    }
}

testMethodは最初に説明した用に作ったやつなので、今回利用しません。

今回はtestMethod2のメソッドを使用します。

testMethod2で呼び出されているsum2メソッドは、Calculatorクラスにあるstaticメソッドです。

 

Calculatorクラス

package com.sample.junittest2;

public class Calculator {

    // publicなメソッド
    public int sum1(int x, int y){
        return x + y;
    }

    // publicでstaticなメソッド
    public static int sum2(int x, int y){
        return x + y;
    }
}

sum1は最初に説明した用に作ったやつなので、今回利用しません。

今回はstaticであるsum2メソッドをモック化します。

 

MainActivityTestクラス

package com.sample.junittest2;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import static junit.framework.TestCase.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

@RunWith(PowerMockRunner.class)
@PrepareForTest({MainActivity.class, Calculator.class})
public class MainActivityTest {

    // テスト対象のクラスを生成
    private MainActivity mainActivity;

    // 初期処理
    @Before
    public void setUp(){
        this.mainActivity = new MainActivity();
    }

    // テスト
    @Test
    public void mockTest2() throws Exception{
        // Calculatorクラスのモックを用意
        PowerMockito.mockStatic(Calculator.class);

        // Mockオブジェクトのスタブを作成
        int stubResult = 1;
        when(Calculator.sum2(any(int.class), any(int.class))).thenReturn(stubResult);

        // テスト対象のメソッド実行
        this.mainActivity.testMethod2();

        // 結果取得
        int result = this.mainActivity.testInt;

        // 評価
        assertEquals(stubResult, result);
    }
}

上記の通りテストクラスを作成するとstaticなメソッドもモック化できます。

ここは順を追って説明していきます(主に最初の説明と異なる部分だけ)

 

@PrepareForTest({MainActivity.class, Calculator.class})

最初の説明で@PrepareForTestにはテスト対象のクラスを指定すると記載しましたが、モック化したいstaticメソッドを持つクラスもここに設定するようです。

今回はCalculatorクラスのstaticメソッドであるsum2をモック化するので、Calculatorクラスを追加してます。

 

        // Calculatorクラスのモックを用意
        PowerMockito.mockStatic(Calculator.class);

Calculatorクラスのモックを用意する記述です。

staticではないメソッドをモック化するときと記述が違います。

staticメソッドをモック化する場合は、mockStaticを使うっぽい。

 

        // Mockオブジェクトのスタブを作成
        int stubResult = 1;
        when(Calculator.sum2(any(int.class), any(int.class))).thenReturn(stubResult);

staticメソッドをモック化してます。

記述方法は以下の通り。

when([staticメソッドを持つクラス(@PrepareForTestで追加したクラス)].[メソッド]).thenReturn([返り値]);

 

これでstaticメソッドがモック化されます。

staticメソッドのモックはstaticではないメソッドと違い、powermockitoのwhenNewを使って注入する必要はないっぽい。

 

スポンサーリンク


 

【オマケ2】

最初に説明した方法はprivateメソッドをモック化することは出来ません。

privateメソッドをモック化する場合、以下のようにします。

前提として、gradle.buildの設定は本記事に書いてある状態にしていることとします。

 

MainActivityクラス

package com.sample.junittest2;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;

import static com.sample.junittest2.Calculator.sum2;

public class MainActivity extends AppCompatActivity {

    public int testInt = 0;
    public int x = 0;
    public int y = 0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void testMethod(){
        Calculator calculator = new Calculator();

        x = 1;
        y = 2;
        testInt = calculator.sum1(x,y);
    }

    public void testMethod2(){
        x = 1;
        y = 2;
        testInt = sum2(x,y);
    }

    public void testMethod3(){
        Calculator calculator = new Calculator();

        x = 1;
        y = 2;
        testInt = calculator.sum3(x,y);
    }
}

testMethodとtestMethod2は最初とオマケ1用に説明する用に作ったやつなので、今回利用しません。

今回はtestMethod3のメソッドを使用します。

testMethod3で呼び出されているsum3メソッドは、Calculatorクラスにあるprivateメソッドを使ったメソッドです。

 

Calculatorクラス

package com.sample.junittest2;

public class Calculator {

    // publicなメソッド
    public int sum1(int x, int y){
        return x + y;
    }

    // publicでstaticなメソッド
    public static int sum2(int x, int y){
        return x + y;
    }

    // privateなメソッド
    public int sum3(int x, int y){
        return privateSum(x, y);
    }

    private int privateSum(int x, int y){
        return x + y;
    }
}

sum1とsum2は最初とオマケ1用に説明する用に作ったやつなので、今回利用しません。

今回はprivateSumメソッドをモック化します。

 

MainActivityTestクラス

package com.sample.junittest2;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import static junit.framework.TestCase.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

@RunWith(PowerMockRunner.class)
@PrepareForTest({MainActivity.class, Calculator.class})
public class MainActivityTest {

    // テスト対象のクラスを生成
    private MainActivity mainActivity;

    // 初期処理
    @Before
    public void setUp(){
        this.mainActivity = new MainActivity();
    }

    // テスト
    @Test
    public void mockTest3() throws Exception{
        // Spyオブジェクト作成
        Calculator spy = PowerMockito.spy(new Calculator());

        // テスト対象のクラスにSpyオブジェクトを注入
        PowerMockito.whenNew(Calculator.class).withNoArguments().thenReturn(spy);

        // Spyオブジェクトのスタブを作成
        int stubResult = 1;
        PowerMockito.doReturn(stubResult).when(spy, "privateSum", any(int.class), any(int.class));

        // テスト対象のメソッド実行
        this.mainActivity.testMethod3();

        // 結果取得
        int result = this.mainActivity.testInt;

        // 評価
        assertEquals(stubResult, result);
    }
}

上記の通りテストクラスを作成するとprivateなメソッドもモック化できます。

ここは順を追って説明していきます(主に最初の説明と異なる部分だけ)

 

@PrepareForTest({MainActivity.class, Calculator.class})

最初の説明で@PrepareForTestにはテスト対象のクラスを指定すると記載しましたが、モック化したいprivateメソッドを持つクラスもここに設定するようです。

今回はCalculatorクラスのprivateメソッドであるprivateSumをモック化するので、Calculatorクラスを追加してます。

 

        // Spyオブジェクト作成
        Calculator spy = PowerMockito.spy(new Calculator());

Calculatorクラスのスパイを用意する記述です。

モックは内部で定義されているメソッドが全て空の状態で作成されますが、スパイは内部で定義されているメソッドが全て元の状態で作成されます。

mockitoのspyではなく、PowerMockitoのspyでspyを作成する必要があります。

 

        // テスト対象のクラスにSpyオブジェクトを注入
        PowerMockito.whenNew(Calculator.class).withNoArguments().thenReturn(spy);

予めテスト対象のクラスに注入しておきます。

この後、スタブを作成しますが、スタブを作成する前に注入します。

 

        // Spyオブジェクトのスタブを作成
        int stubResult = 1;
        PowerMockito.doReturn(stubResult).when(spy, "privateSum", any(int.class), any(int.class));

spyオブジェクトとして作成したCalculatorクラスの中で、privateSumメソッドだけ再定義します。

記述方法は以下の通り。

PowerMockito.doReturn([返り値]).when([モック(スパイ)オブジェクト], "[定義するメソッドの名前]", [引数]);

 

これでprivateメソッドがモック化されます。

 

他のプログラミングに関する記事はこちら

スポンサーリンク