概要
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
参考
- [mockito] (http://site.mockito.org/)
- [mockito/mockito - GitHub Wiki] (https://github.com/mockito/mockito/wiki)
- [Mockito 2.13.0 API] (https://static.javadoc.io/org.mockito/mockito-core/2.13.0/org/mockito/Mockito.html)
導入
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)] (https://static.javadoc.io/org.mockito/mockito-core/2.13.0/org/mockito/Mockito.html#39)にあるように、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] (https://static.javadoc.io/org.mockito/mockito-core/2.13.0/org/mockito/junit/MockitoJUnitRunner.html)
@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] (https://static.javadoc.io/org.mockito/mockito-core/2.13.0/org/mockito/junit/MockitoRule.html)
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(answer = Answers.RETURNS_SMART_NULLS)
private lateinit var bar: Bar
mock(Bar::class.java, Answers.RETURNS_SMART_NULLS)
指定できる種類は[Enum Answers] (https://static.javadoc.io/org.mockito/mockito-core/2.13.0/org/mockito/Answers.html)で定義されています。
以下の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] (https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-pair/index.html)のことです。
- 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が付いているためです。
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] (https://static.javadoc.io/org.mockito/mockito-core/2.13.0/org/mockito/Mock.html)
@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)] (https://static.javadoc.io/org.mockito/mockito-core/2.13.0/org/mockito/Mockito.html#mock-java.lang.Class-)
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] (https://static.javadoc.io/org.mockito/mockito-core/2.13.0/org/mockito/stubbing/OngoingStubbing.html#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] (https://static.javadoc.io/org.mockito/mockito-core/2.13.0/org/mockito/Spy.html)
@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)] (https://static.javadoc.io/org.mockito/mockito-core/2.13.0/org/mockito/Mockito.html#spy-java.lang.Object-)
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] (https://static.javadoc.io/org.mockito/mockito-core/2.13.0/org/mockito/InjectMocks.html)
@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] (https://github.com/mockito/mockito/wiki/FAQ#what-are-the-limitations-of-mockito)
- Requires Java 6+
- 静的メソッドはmockできない
- コンストラクタはmockできない
- equals、hashCodeメソッドはmockできない
- モックは[ObjenesisによってサポートされているVM] (https://github.com/easymock/objenesis/blob/2.5/SupportedJVMs.md)でのみ可能
- Spying on real methods where real implementation references outer Class via OuterClass.this is impossible.