LoginSignup
22
23

More than 5 years have passed since last update.

ユニットテストでmocking frameworkにMockito 2.13を利用するサンプルコード

Last updated at Posted at 2018-02-09

概要

Kotlinを利用したプロジェクトのユニットテストでMockフレームワークにMockito 2.13を使ったサンプルコードです。

環境

  • Windows 10 Professional
  • Java 1.8.0_162
  • Kotlin 1.2.21
  • JUnit 4.12
  • Mockito 2.13.0
  • IntelliJ IDEA 2017.3

参考

導入

dependencies {

    testCompile("org.mockito:mockito-core:2.13.0")
}

テストクラスで、Mockitoクラスのstaticメソッドをimportしておくとコード量が減って見やすくなります。

import org.mockito.Mockito.*

final class/methodをmock/spy化できるようにする

デフォルトではfinal class/methodはmock/spy化できませんが、39. Mocking final types, enums and final methods (Since 2.1.0)にあるように、Mockito 2.1.0から下記の設定を行うことでfinal class/methodのmock/spy化ができるようになっています。

手順

以下の場所にmockito-extensionsというディレクトリを作成します。

test/resources

そこへorg.mockito.plugins.MockMakerという名前のテキストファイルを作成し下記の1行を追加します。

mock-maker-inline

これでfinalなclassもmock/spy化できますが、Stringなど特定のクラスはmock/spy化できないようです。

org.mockito.exceptions.base.MockitoException: 
Cannot mock/spy class java.lang.String
Mockito cannot mock/spy because :
 - Cannot mock wrapper types, String.class or Class.class

オブジェクトを初期化する

Mock、Spy、Captor、InjectMocksアノテーションが付いたオブジェクトの初期化を行う方法です。
下記の方法があり状況に応じて選択できます。

MockitoJUnitRunnerを使用する

Class MockitoJUnitRunner

@RunWith(MockitoJUnitRunner::class)
class Tests {
} 

推奨する設定 (Highly recommended)

It is highly recommended to use MockitoJUnitRunner.StrictStubs variant of the runner. It drives cleaner tests and improves debugging experience.

@RunWith(MockitoJUnitRunner.StrictStubs::class)

StrictStubsにすると

引数のミスマッチを検出します。
次のようなstubの設定と実際の呼び出しに引数の違いがあった場合

`when`(mock.sumAndApplyRate(arrayOf(1, 2, 3))).thenReturn(60)

mock.sumAndApplyRate(arrayOf(1, 2, 4)).also { println(it) }

例外がスローされます。

org.mockito.exceptions.misusing.PotentialStubbingProblem: 
Strict stubbing argument mismatch. Please check:
 - this invocation of 'sumAndApplyRate' method:
    fugaLogic.sumAndApplyRate([1, 2, 4]);
    -> at com.example.exercise.mockito.BasicMockitoExerciseTests.test demo mock3(BasicMockitoExerciseTests.kt:200)
 - has following stubbing(s) with different arguments:
    1. fugaLogic.sumAndApplyRate([1, 2, 3]);
      -> at com.example.exercise.mockito.BasicMockitoExerciseTests.test demo mock3(BasicMockitoExerciseTests.kt:196)

呼び出されないstubメソッドを検出します。
設定したstubメソッドが呼び出されないと例外がスローされます。

org.mockito.exceptions.misusing.UnnecessaryStubbingException: 
Unnecessary stubbings detected.
Clean & maintainable test code requires zero unnecessary code.
Following stubbings are unnecessary (click to navigate to relevant line of code):
  1. -> at com.example.exercise.mockito.BasicMockitoExerciseTests.test demo mock2(BasicMockitoExerciseTests.kt:175)
Please remove unnecessary stubbings or use 'silent' option. More info: javadoc for UnnecessaryStubbingException class.

MockitoRuleを使用する

Interface MockitoRule

class Tests {

    @Rule
    @JvmField
    val rule: MockitoRule = MockitoJUnit.rule()

}

推奨する設定 (Highly recommended)

テストランナーと同じ設定がルールでも行えます。
Incubatingアノテーションが付いているので将来仕様が変わることがあります。

@Rule
@JvmField
val rule: MockitoRule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS)

MockitoAnnotations.initMocksを使用する

テストランナーやルールを使用しない場合はinitMocksで初期化することができます。

class Tests {

    @Mock
    private lateinit var hoge: Hoge
    @Spy
    private lateinit var fuga: Fuga

    @Before
    fun setup() {
        MockitoAnnotations.initMocks(this)
    }

}

上記のいずれの方法も利用しない

アノテーションを使わずにmock/spyオブジェクトを手動で生成し、テスト対象のオブジェクトに手動で設定(注入)するという方法もあります。

@Test
fun `test`() {

    val bar = mock(Bar::class.java)
    val foo = spy(FooImpl(123))

    val sut = MockDemoServiceImpl(bar, foo)

    //テストする
}

Mockオブジェクト

テスト対象のオブジェクトが依存するオブジェクトの代用オブジェクトで、テスト対象が依存オブジェクトの呼び出しを期待した通りに行ったか検証する目的で利用します。
メソッドの戻り値を予め決めた値にしたり特定の引数で例外をスローさせたりとオブジェクトの振る舞いを制御することもできます。
またインターフェースもmock化できるので実装クラスがなくてもテストが可能という利点もあります。
Mockitoの場合、mock化したオブジェクトのメソッドはデフォルトでstub(呼び出しに対して予め決めた値を返す)なので、stubオブジェクトの性質もあわせもっているといえます。

デフォルトの戻り値

mock化したオブジェクトのメソッドの戻り値は、型毎にデフォルトが決まっています。どのような値を戻すかはオブジェクトをmock化する際に指定することができます。
通常はデフォルト(RETURNS_DEFAULTS)の設定でいいようですが、Mockito 3.0でデフォルトになる予定のRETURNS_SMART_NULLSという「nullの代わりになるべくSmartNullを返す」設定もあります。

RETURNS_SMART_NULLSの設定例

Mockアノテーション
@Mock(answer = Answers.RETURNS_SMART_NULLS)
private lateinit var bar: Bar
mockメソッド
mock(Bar::class.java, Answers.RETURNS_SMART_NULLS)

指定できる種類はEnum Answersで定義されています。

以下の2つについて、戻り値の型毎にどのようなデフォルト値を返すか表にまとめました。

  • RETURNS_DEFAULTS (デフォルト)
  • RETURNS_SMART_NULLS
RETURNS_DEFAULTS RETURNS_SMART_NULLS
String null ""
String? null ""
Int 0 0
Int? 0 0
Date null SmartNull
Date? null SmartNull
LocalDate null null
LocalDate? null null
Boolean false false
Boolean? false false
Array null size 0
Array? null size 0
List size 0 size 0
List? size 0 size 0
Pair null null
Pair? null null
CustomData null SmartNull
CustomData? null SmartNull
  • Pair : Kotlinの標準ライブラリのPiarのことです。
  • CustomData : 検証用に用意したクラスです。
    • open class CustomData(val id: Long, val name: String)

RETURNS_DEFAULTS

メソッドの戻り値の型がプリミティブの場合はその型の初期値、コレクションはempty、オブジェクトの場合はnullを返します。

RETURNS_SMART_NULLS

ReturnsSmartNulls first tries to return ordinary values (zeros, empty collections, empty string, etc.) then it tries to return SmartNull. If the return type is final then plain null is returned.

RETURNS_DEFAULTSではStringの場合はnullを返しますが、RETURNS_SMART_NULLSでは空文字を返すように、極力nullを返さないようになっています。
オブジェクト型もnullの代わりになるべくSmartNullを返すようになりますが、final修飾子が付いている場合はnullになります。上記の表でLocalDateやPairがSmartNullにならないのは、このクラスにfinalが付いているためです。

signature
public final class LocalDate
        implements Temporal, TemporalAdjuster, ChronoLocalDate, Serializable

SmartNullを返すメソッドの戻り値を使用するとNullPointerExceptionの代わりに、下記のようにSmartNullPointerExceptionがスローされます。
エラーメッセージもNPEより詳しく出力されるので原因の確認が効率的にできます。

org.mockito.exceptions.verification.SmartNullPointerException: 
You have a NullPointerException here:
-> at com.example.exercise.mockito.BasicMockitoExerciseTests.test hoge(BasicMockitoExerciseTests.kt:97)
because this method call was *not* stubbed correctly:
-> at com.example.exercise.mockito.BasicMockitoExerciseTests.test hoge(BasicMockitoExerciseTests.kt:96)
hogeLogic.getDate();

mockオブジェクトの生成

Mockアノテーションを使う方法

Annotation Type Mock

@Target({FIELD, PARAMETER})
@Retention(RUNTIME)
@Documented
public @interface Mock
@Mock
private lateinit var bar: Bar

mockオブジェクトに名前を付けます。付けた名前はデバッグで利用できます。

@Mock(name = "name of bar")
private lateinit var bar: Bar

Answerを設定します。

@Mock(answer = Answers.RETURNS_SMART_NULLS)
private lateinit var bar: Bar

Mockito.mockメソッドを使う方法

Mockito.mock(Class)

signature
public static <T> T mock(Class<T> classToMock)
public static <T> T mock(Class<T> classToMock, String name)
public static <T> T mock(Class<T> classToMock, Answer defaultAnswer)
public static <T> T mock(Class<T> classToMock, MockSettings mockSettings)

アノテーションで生成するのと同様です。

private val bar = mock(Bar::class.java)

private val bar = mock(Bar::class.java, "name of bar")

private val bar = mock(Bar:class.java, Answers.RETURNS_SMART_NULLS)

メソッドの振る舞いを設定する

whenメソッドでメソッドが任意の値を返すようにしたり、例外をスローさせたりといった振る舞いを設定します。

メソッドの戻り値を設定する (thenReturn)

実行時の現在日時を返すようなメソッドでも固定した日時を返すようにすることができます。

`when`(hoge.currentDateTime()).thenReturn(LocalDateTime.of(2018,2,1,0,0,0))

hoge.currentDateTime().also { println(it) }
// 2018-02-01T00:00

メソッドの戻り値を100という固定値に設定します。

`when`(hoge.count).thenReturn(100)

hoge.count.also { println(it) }
// 100

メソッドの呼び出し毎に戻り値を変える

thenReturnに複数の値を設定することで呼び出し毎に戻り値を変えます。

`when`(hoge.count).thenReturn(100, 101)

hoge.count.also { println(it) }
// 100
hoge.count.also { println(it) }
// 101
hoge.count.also { println(it) }
// 101

引数の値によって特定の値を返す

メソッド呼び出し時に引数のマッチングで特定の値を返すようにするにはArgumentMatchersクラスを使用します。

この例では引数がaから始まる文字列の場合に1を返すように振る舞いを設定しています。
この条件にマッチしない呼び出しの時はInt型のデフォルト値の0が返ります。

`when`(hoge.fruits(startsWith("a"))).thenReturn(1)

hoge.fruits("apple").also { println(it) }
// 1
hoge.fruits("banana").also { println(it) }
// 0

1つのメソッドに複数の振る舞いを持たせる場合は少し記述方法が変わります。
ポイントは書き方がwhen().thenReturn()ではなくdoReturn().when()になるのと、マッチングの範囲が最も広い条件を先頭にすることです。

doReturn(3).`when`(hoge).fruits(anyString())
doReturn(1).`when`(hoge).fruits(startsWith("a"))
doReturn(2).`when`(hoge).fruits(startsWith("b"))

hoge.fruits("apple").also { println(it) }
// 1
hoge.fruits("banana").also { println(it) }
// 2
hoge.fruits("cherry").also { println(it) }
// 3
hoge.fruits("durian").also { println(it) }
// 3

メソッド呼び出しで例外をスローさせる (thenThrow)

メソッド呼び出し時に任意の例外をスローさせます。引数の値で例外をスローさせるかどうかも設定することができます。

この例では引数が0ときに例外をスローさせます。なお、引数が0以外の場合は条件に一致しないので例外はスローされません。

`when`(hoge.initCount(0)).thenThrow(IllegalStateException())

hoge.initCount(0)
// IllegalStateExceptionがスローされる

hoge.initCount(1)
// 例外はスローされない

ArgumentMatchers.anyInt()を使うと任意の数値がマッチします。

`when`(hoge.initCount(anyInt())).thenThrow(IllegalStateException())

hoge.initCount(100)
// IllegalStateExceptionがスローされる

メソッド呼び出しのx回目で例外をスローさせるということもできます。
この例では2回目のメソッド呼び出しで例外をスローさせます。
ちなみにdoNothingを使っているのはメソッドが戻り値を返さないためです。

doNothing().doThrow(IllegalStateException()).`when`(hoge).initCount(anyInt())

hoge.initCount(10)
hoge.initCount(20)
// IllegalStateExceptionがスローされる

メソッドの呼び出しを検証する

verifyメソッドでテスト対象がmock化した依存オブジェクトを正しく呼び出しているか検証します。

メソッドが呼び出されたことを検証

verify(hoge).countUp()

メソッドがx回呼び出されたことを検証

verify(hoge, times(2)).countUp()

メソッドが最大x回まで呼び出されたことを検証

verify(hoge, atMost(2)).countUp()

メソッドが最低x回以上呼び出されたことを検証

verify(hoge, atLeast(2)).countUp()

このメソッドだけが呼び出されたことを検証

verify(hoge, only()).countUp()

メソッドが呼び出されなかったことを検証

verify(hoge, never()).countUp()

検証が失敗した時の説明を付ける

verify(hoge, never().description("このメソッドはxxxの時、呼び出されない")).countUp()
org.mockito.exceptions.base.MockitoAssertionError: このメソッドはxxxの時、呼び出されない

hogeLogic.countUp();
Never wanted here:
-> at com.example.exercise.mockito.HogeLogic.countUp(HogeLogic.kt:102)
But invoked here:
-> at com.example.exercise.mockito.BasicMockitoExerciseTests.test demo mock5(BasicMockitoExerciseTests.kt:225)

部分的なモック化 (Real partial mocks)

特定のstubメソッドの振る舞いをthenCallRealMethodにすると、そのメソッドを呼び出したときに実際のオブジェクトのメソッドを呼び出します。

ただし、JavaDocのサンプルコードのコメントに注意書きがあるようにオブジェクトの状態に依存しているメソッドは、期待しない結果になる場合があります。

//you can enable partial mock capabilities selectively on mocks:
val mock = Mockito.mock(Foo::class.java)

//Be sure the real implementation is 'safe'.
//If real implementation throws exceptions or depends on specific state of the object then you're in trouble.
`when`(mock.someMethod()).thenCallRealMethod()

stubオブジェクト

mockオブジェクトと似た性質を持っていますが、メソッド呼び出しの検証はできません。
なので、依存オブジェクトの振る舞いを制御可能な状態にするだけで検証は不要な場合に利用します。
MockitoにはStubアノテーションやMockito.stubというメソッドがないので、明示的にStubオブジェクトというものは作れません。

stubオブジェクトの生成

Mockitoでは以下の方法でstubと呼べるオブジェクトを生成できます。

Mockアノテーションを使う方法

@Mock(stubOnly = true)
private lateinit var bar: Bar

Mockito.mockメソッドを使う方法

private val bar = mock(Bar::class.java, withSettings().stubOnly())

メソッドの振る舞いを設定する

mockオブジェクトと同様です。

メソッドの呼び出しを検証する

stubオブジェクトなのでverifyメソッドで検証することはできません。
コンパイルエラーにはなりませんが、verifyで検証しようとすると例外がスローされます。

org.mockito.exceptions.misusing.CannotVerifyStubOnlyMock: 
Argument passed to verify() is a stubOnly() mock, not a full blown mock!
If you intend to verify invocations on a mock, don't use stubOnly() in its MockSettings.

spyオブジェクト

mockオブジェクトと違いspyオブジェクトはオブジェクトの状態を持っているので、依存オブジェクトに対する副作用も含めて検証する場合に利用します。

spyオブジェクトの生成

mockオブジェクトと違いspyオブジェクトの生成には実際の依存オブジェクトが必要です。
Spyアノテーションで生成する場合はデフォルトコンストラクタが必要です。
デフォルトコンストラクタがない場合は、手動でオブジェクトを生成して初期化する必要があります。

Spyアノテーションを使う方法

Annotation Type Spy

@Retention(RUNTIME)
@Target(FIELD)
@Documented
public @interface Spy

デフォルトコンストラクタがある場合はMockitoがオブジェクトを生成してくれます。

@Spy
private lateinit var bar: Bar

オブジェクトの生成に引数が必要な場合は手動で生成します。

@Spy
private val foo = Far("argument")

Mockito.spyメソッドを使う方法

Mockito.spy(Class)

signature
public static <T> T spy(T object)

spyメソッドを使う場合はいずれの場合も手動でオブジェクトを生成する必要があります。

private val bar = spy(Bar())
private val foo = spy(Foo("argument"))

メソッドの振る舞いをstub化する

mockオブジェクトと同様にspyオブジェクトのメソッドもstub化できますが注意する点があります。

1つ目は、MockitoのAPIリファレンスにも注意書きがあるとおり、以下のようなコードだとwhenメソッドの実行時にIndexOutOfBoundsExceptionがスローされます。

val list = mutableListOf<String>()
val spyList = spy(list)

`when`(spyList[0]).thenReturn("FOO")
// java.lang.IndexOutOfBoundsException: Index: 0, Size: 0

val actual = spyList[0]

assertThat(actual).isEqualTo("FOO")
verify(spyList, times(1))[0]

代わりに下記のように記述すると例外は発生しません。

doReturn("FOO").`when`(spyList)[0]

もう1つは、stub化したメソッドがオブジェクトの状態を変えるような場合、その副作用は起きなくなります。

val list = mutableListOf<String>("foo", "bar", "qix")
val spyList = spy(list)

doNothing().`when`(spyList).clear()

spyList.clear()

spyList.joinToString().also { println(it) }
// foo, bar, qix

テスト対象オブジェクトの生成

テスト対象(SUT / System under test)オブジェクトの生成時に、mock/spyオブジェクトを注入するにはInjectMocksアノテーションを使う方法があります。
mock/spyオブジェクトの注入には、コンストラクタインジェクション、セッターインジェクション、フィールドインジェクションの方法があります。

InjectMocksアノテーションを使う方法

InjectMocks

@Documented
@Target(value=FIELD)
@Retention(value=RUNTIME)
public @interface InjectMocks
@Mock
private lateinit var bar: Bar
@Spy
private val foo: Foo = FooImpl(123)

@InjectMocks
private lateinit var sut: MockDemoServiceImpl

補足

Mockito 2.xの制約

What are the limitations of Mockito

  • Requires Java 6+
  • 静的メソッドはmockできない
  • コンストラクタはmockできない
  • equals、hashCodeメソッドはmockできない
  • モックはObjenesisによってサポートされているVMでのみ可能
  • Spying on real methods where real implementation references outer Class via OuterClass.this is impossible.
22
23
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
23