Mockito とは
- Java のテストでモックを簡単に扱えるようにするためのフレームワーク
- 指定されたクラスのモックを作成し、任意のメソッドをスタブ化して指定した値を返すようにしたり、モックのメソッドが期待した通りに呼び出されたかどうかを検証したりできる
-
static
やfinal
なメソッドのモック化もできたりする(昔はできなかったはずだけど、いつの頃からかできるようになってた) - モックの振る舞いの定義を型安全に定義できるのが大きな特徴(だと思う)
- 読み方は「もきーと」
環境
> gradle --version
------------------------------------------------------------
Gradle 7.4.2
------------------------------------------------------------
Build time: 2022-03-31 15:25:29 UTC
Revision: 540473b8118064efcc264694cbcaa4b677f61041
Kotlin: 1.5.31
Groovy: 3.0.9
Ant: Apache Ant(TM) version 1.10.11 compiled on July 10 2021
JVM: 18.0.1.1 (Oracle Corporation 18.0.1.1+2-6)
OS: Windows 10 10.0 amd64
Hello World
実装
plugins {
id "java"
}
sourceCompatibility = 18
targetCompatibility = 18
[compileJava, compileTestJava]*.options*.encoding = "UTF-8"
repositories {
mavenCentral()
}
dependencies {
testImplementation "org.mockito:mockito-core:4.6.1"
testImplementation "org.junit.jupiter:junit-jupiter:5.8.2"
}
test {
useJUnitPlatform()
}
package sandbox.mockito;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class HelloMockitoTest {
@Test
void test() {
List<String> mock = mock(List.class);
when(mock.get(0)).thenReturn("Hello Mockito!");
System.out.println(mock.get(0));
}
}
Hello Mockito!
説明
dependencies {
testImplementation "org.mockito:mockito-core:4.6.1"
...
}
- org.mockito:mockito-core を依存関係に設定する
List<String> mock = mock(List.class);
-
Mockito#mock(Class<?>)
で、指定した型のモックオブジェクトを生成する - ここでは、
java.util.List
のモックオブジェクトを生成している
when(mock.get(0)).thenReturn("Hello Mockito!");
-
Mockito#when(T)
などの API を使い、モックの振る舞いを定義する - ここでは、モックの
get(0)
メソッドを実行したら、"Hello Mockito!"
という文字列を返すように定義している-
when()
メソッドの引数で、スタブ化したいメソッドの振る舞いを実行するように実装する -
thenReturn(T)
で、when()
メソッドの引数で実行したメソッドが返す値を指定する -
when(T)
が受け取る型とthenReturn(T)
が受け取る型は一致するようになっているので、間違った型を指定した場合はコンパイルエラーになる(型安全)
-
モックのデフォルトの振る舞い
package sandbox.mockito;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.mockito.Mockito.mock;
public class DefaultReturnValueTest {
@Test
void test() {
Hoge mock = mock(Hoge.class);
System.out.println("mock.anyObject() = " + mock.anyObject());
System.out.println("mock.primitive() = " + mock.primitive());
System.out.println("mock.primitiveWrapper() = " + mock.primitiveWrapper());
System.out.println("mock.array() = " + mock.array());
System.out.println("mock.collection() = " + mock.collection());
}
public class Hoge {
public String anyObject() {
System.out.println("Hello World");
return "Hello World";
}
public int primitive() {
return 1;
}
public Integer primitiveWrapper() {
return 2;
}
public int[] array() {
return new int[] {1, 2, 3};
}
public List<String> collection() {
return List.of("hello", "world");
}
}
}
mock.anyObject() = null
mock.primitive() = 0
mock.primitiveWrapper() = 0
mock.array() = null
mock.collection() = []
- モックのメソッドは、本来の処理を一切行わなくなる
- モックのメソッドは、以下のように値を返すようになる
- プリミティブ型・ラッパー型
- デフォルト値
-
int
なら0
,boolean
ならfalse
など
- コレクション型
- 空のコレクション
- 配列・その他オブジェクト
null
- プリミティブ型・ラッパー型
スタブを定義する
package sandbox.mockito;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class WhenTest {
@Test
void test() {
List<String> mock = mock(List.class);
when(mock.get(0)).thenReturn("hello");
when(mock.get(1)).thenReturn("world");
System.out.println("mock.get(0) = " + mock.get(0));
System.out.println("mock.get(0) = " + mock.get(0));
System.out.println("mock.get(0) = " + mock.get(0));
System.out.println("mock.get(1) = " + mock.get(1));
System.out.println("mock.get(2) = " + mock.get(2));
when(mock.get(1)).thenReturn("Override");
System.out.println("mock.get(1) = " + mock.get(1));
}
}
mock.get(0) = hello
mock.get(0) = hello
mock.get(0) = hello
mock.get(1) = world
mock.get(2) = null
mock.get(1) = Override
-
when(<スタブ化したいメソッドの呼び出し>).thenReturn(<メソッドが返す値>)
とすることで、モックの任意のメソッドが返す値を指定できる - 一度スタブ化されたメソッドは、常に同じ値を返すようになる
- スタブ化は引数も条件に含まれているため、スタブ化したときとは異なる引数でメソッドを実行した場合は指定された値は返されない
- スタブは上書きが可能で、同じ条件でスタブを再定義すると新しく指定された値を返すようになる
実行するごとに異なる値を返すようにスタブ化する
@Test
void testConsecutiveCalls() {
List<String> mock = mock(List.class);
when(mock.get(0)).thenReturn("one", "two", "three");
System.out.println("mock.get(0) = " + mock.get(0));
System.out.println("mock.get(0) = " + mock.get(0));
System.out.println("mock.get(0) = " + mock.get(0));
System.out.println("mock.get(0) = " + mock.get(0));
}
mock.get(0) = one
mock.get(0) = two
mock.get(0) = three
mock.get(0) = three
- 引数が可変長引数の
thenReturn(T...)
を使用すれば、実行するごとにメソッドが返す値が変わるようにスタブ化できる - 指定された数以上メソッドが実行された場合は、最後に指定した値を返し続ける
例外をスローするようにスタブ化する
@Test
void testThrowException() {
List<String> mock = mock(List.class);
when(mock.get(0)).thenThrow(new RuntimeException("test"));
try {
mock.get(0);
} catch (Throwable e) {
System.out.println("e = " + e);
}
}
e = java.lang.RuntimeException: test
-
thenThrow(Throwable)
を使用すると、メソッドを実行したときに例外をスローするようにスタブ化できる
実行するごとに異なる例外をするようにスタブ化する
@Test
void testConsecutiveThrowException() {
List<String> mock = mock(List.class);
when(mock.get(0)).thenThrow(
new RuntimeException("one"),
new NullPointerException("two"),
new IllegalArgumentException("three")
);
try {
mock.get(0);
} catch (Throwable e1) {
System.out.println("e1 = " + e1);
}
try {
mock.get(0);
} catch (Throwable e2) {
System.out.println("e2 = " + e2);
}
try {
mock.get(0);
} catch (Throwable e3) {
System.out.println("e3 = " + e3);
}
try {
mock.get(0);
} catch (Throwable e4) {
System.out.println("e4 = " + e4);
}
}
e1 = java.lang.RuntimeException: one
e2 = java.lang.NullPointerException: two
e3 = java.lang.IllegalArgumentException: three
e4 = java.lang.IllegalArgumentException: three
- 引数が可変長引数の
thenThrow(Throwable...)
を使用すれば、実行するごとにメソッドがスローする例外が変わるようにスタブ化できる - 指定された数以上メソッドが実行された場合は、最後に指定した例外をスローし続ける
戻り値と例外のスローを組み合わせてスタブ化する
@Test
void testConsecutiveThrowAndReturn() {
List<String> mock = mock(List.class);
when(mock.get(0))
.thenReturn("one")
.thenThrow(new RuntimeException("two"))
.thenReturn("three");
System.out.println("mock.get(0) = " + mock.get(0));
try {
mock.get(0);
} catch (Throwable e) {
System.out.println("e = " + e);
}
System.out.println("mock.get(0) = " + mock.get(0));
}
mock.get(0) = one
e = java.lang.RuntimeException: two
mock.get(0) = three
-
thenReturn()
およびthenThrow()
をメソッドチェーンでつなげることで、戻り値の指定と例外のスローの指定を組み合わせてスタブを定義できる - スタブの定義は上書きされるので、チェーンさせずに個別に定義すると最後の定義だけが有効になるので注意
when(mock.get(0)).thenReturn("one"); // 上書きされる
when(mock.get(0)).thenThrow(new RuntimeException("two")); // 上書きされる
when(mock.get(0)).thenReturn("three"); // これだけ有効になる
- こう定義してしまうと、最後の
thenReturn("three")
だけが有効になってしまう、ということ
任意の引数に対してスタブ化させる
@Test
void testAnyArgumentMatch() {
List<String> mock = mock(List.class);
when(mock.get(anyInt())).thenReturn("hoge");
System.out.println("mock.get(0) = " + mock.get(0));
System.out.println("mock.get(1) = " + mock.get(1));
System.out.println("mock.get(2) = " + mock.get(2));
}
mock.get(0) = hoge
mock.get(1) = hoge
mock.get(2) = hoge
-
ArgumentMatchers に定義されている
any
で始まるメソッド(anyInt()
,anyList()
,any()
など)を引数に使用してスタブを定義すると、任意の引数でのメソッド実行をスタブ化できる
ArgumentMachers に用意されているマッチ用のメソッド
ArgumentMatchers
には、よく使いそうなマッチ用のメソッドが用意されている。
anyInt()
や anyCollection()
のような any型名()
は、null
以外の 型名
の型に一致する任意の値にマッチする。
以下で、 any型名()
以外のメソッドについて、マッチの条件をまとめる。
(eq()
や ~That()
は使いどころがちょっと違うので、次の項で説明)
メソッド | 説明 |
---|---|
startsWith(String) |
指定した文字列で始まる |
endsWith(String) |
指定した文字列で終わる |
contains(String) |
指定した文字列を含む |
matches(String) |
指定した正規表現にマッチする |
isNull() |
引数がnull である |
isNotNull() , notNull()
|
引数がnull でない |
any() |
null を含む、あらゆる値にマッチする |
any(Class<T>) , isA(Class<T>)
|
null 以外の、指定された型の任意の値にマッチする |
nullable(Class<T>) |
null を含む、指定された型の任意の値にマッチする |
same(T) |
指定したオブジェクトと同じオブジェクトかどうか |
nullable()
と any()
の違いはよく分からない。
複数ある引数のうち一部だけにマッチングを適用する
@Test
void testStubbingMultipleArgumentsMethod_invalidCase() {
BiFunction<String, String, String> mock = mock(BiFunction.class);
when(mock.apply("hello", anyString())).thenReturn("mocked");
System.out.println(mock.apply("hello", "world"));
}
- 第二引数だけ任意の文字列にしようとしている
Invalid use of argument matchers!
2 matchers expected, 1 recorded:
-> at sandbox.mockito.StubbingTest.testStubbingMultipleArgumentsMethod(StubbingTest.java:200)
This exception may occur if matchers are combined with raw values:
//incorrect:
someMethod(any(), "raw String");
When using matchers, all arguments have to be provided by matchers.
For example:
//correct:
someMethod(any(), eq("String by matcher"));
For more info see javadoc for Matchers class.
org.mockito.exceptions.misusing.InvalidUseOfMatchersException:
Invalid use of argument matchers!
2 matchers expected, 1 recorded:
-> at sandbox.mockito.StubbingTest.testStubbingMultipleArgumentsMethod(StubbingTest.java:200)
This exception may occur if matchers are combined with raw values:
//incorrect:
someMethod(any(), "raw String");
When using matchers, all arguments have to be provided by matchers.
For example:
//correct:
someMethod(any(), eq("String by matcher"));
...
- エラーになる
- 引数が複数あるメソッドをスタブ化するときに1つでもマッチングで条件を設定した場合は、他のすべての引数も同じようにマッチングで条件を設定しなければ上記のようなエラーになる
@Test
void testStubbingMultipleArgumentsMethod_validCase() {
BiFunction<String, String, String> mock = mock(BiFunction.class);
when(mock.apply(eq("hello"), anyString())).thenReturn("mocked");
System.out.println(mock.apply("hello", "world"));
}
mocked
-
ArgumentMatchers
のeq()
メソッドを使って、第一引数の方もマッチング形式で条件を設定すればエラーは発生しなくなる
自作のマッチング処理を指定する
@Test
void testCustomArgumentMatcher() {
Function<String, String> mock = mock(Function.class);
ArgumentMatcher<String> isHoge = arg -> arg.equals("hoge");
when(mock.apply(argThat(isHoge))).thenReturn("HOGE!!!");
System.out.println("mock.apply(hoge) = " + mock.apply("hoge"));
System.out.println("mock.apply(fuga) = " + mock.apply("fuga"));
}
mock.apply(hoge) = HOGE!!!
mock.apply(fuga) = null
-
ArgumentMatchers
のargThat(ArgumentMatcher)
を使用すると、マッチング処理を実装した任意の ArgumentMatcher を使って条件を指定できる
スタブ化したメソッド内で任意の処理を実行する
@Test
void testAnswer() {
Function<String, String> mock = mock(Function.class);
Answer<String> answer = invocation -> {
System.out.println("arguments = " + Arrays.toString(invocation.getArguments()));
return "world";
};
when(mock.apply("hello")).thenAnswer(answer);
System.out.println("mock.apply(hello) = " + mock.apply("hello"));
System.out.println("mock.apply(hey) = " + mock.apply("hey"));
}
arguments = [hello]
mock.apply(hello) = world
mock.apply(hey) = null
-
thenAnswer(Answer)
を使用すると、スタブ化したメソッドが呼ばれたときに実行する処理を指定できる - スタブの処理は Answer で実装する
-
answer(InvocationOnMock)
メソッドを実装する - 引数で受け取る InvocationOnMock から、実際に渡された引数を取り出すことができる
-
-
Answer1, Answer2, Answer3 というインタフェースが用意されていて、引数を型安全に受け取りたい場合に利用できる
- Answer6 まである
import static org.mockito.AdditionalAnswers.answer;
...
public class StubbingTest {
...
@Test
void testAnswer1() {
Function<String, String> mock = mock(Function.class);
Answer1<String, String> answer1 = arg -> {
System.out.println("arg = " + arg);
return "world";
};
when(mock.apply("hello")).thenAnswer(answer(answer1));
System.out.println("mock.apply(hello) = " + mock.apply("hello"));
System.out.println("mock.apply(hey) = " + mock.apply("hey"));
}
arg = hello
mock.apply(hello) = world
mock.apply(hey) = null
-
AdditionalAnswers の
answer()
にAnswer1
~Answer6
を渡して使用する -
Answer1
などでは、InvocationOnMock
ではなく渡された引数を直接受け取ることができる
スタブが色々処理をしすぎると、テストの意味が無くなったりメンテナンスがしづらくなる(私見)ので、乱用は避けた方がいい。
公式でも、極力 thenReturn()
や thenThrow()
だけでスタブを定義することが推奨されている。
We recommend simply stubbing with thenReturn() or thenThrow(), which should be enough to test/test-drive any clean and simple code.
戻り値が void のメソッドをスタブ化(例外をスローさせるように)する
@Test
void testReturnVoid() {
Runnable mock = mock(Runnable.class);
doThrow(new RuntimeException("test")).when(mock).run();
try {
mock.run();
} catch (Throwable e) {
System.out.println("e = " + e);
}
}
e = java.lang.RuntimeException: test
-
when(スタブ化したいメソッドの呼び出し)
の方法は、スタブ化したいメソッドが戻り値を持つことが前提となっている- 戻り値が
void
のメソッドだと、when()
に渡してもコンパイルエラーになる
- 戻り値が
- 戻り値が
void
のメソッドが例外をスローするようにスタブ化したい場合は、doThrow()
から始まるスタブ化の方法を利用する-
doThrow()
の引数に、スローさせたい例外を設定する - 続けて、
when()
の引数にモックオブジェクトを渡す -
when()
の戻り値の型は引数で渡したモックオブジェクトと同じ型になるようになっているので、スタブ化したいメソッド呼び出しをメソッドチェーンで続けて実行する
-
メソッドが呼び出されたことを検証する
package sandbox.mockito;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
public class VerifyTest {
@Test
void test() {
List<String> mock = mock(List.class);
mock.get(1);
verify(mock).get(0);
}
}
Argument(s) are different! Wanted:
list.get(0);
-> at sandbox.mockito.VerifyTest.test(VerifyTest.java:17)
Actual invocations have different arguments:
list.get(1);
-> at sandbox.mockito.VerifyTest.test(VerifyTest.java:15)
-
Mockito#verify(T)
メソッドを使用することで、モックのメソッドが1 回だけ実行されたことを検証できる-
verify()
メソッドの引数にモックオブジェクトを渡す -
verify()
の戻り値の型は引数で渡したモックオブジェクトと同じ型になっており、メソッドチェーンでメソッドを実行することで、そのメソッドが実行されたかどうかを検証できる - 検証は引数を含めて行われる
-
- 上記実装例では、
get(0)
が 1 回だけ実行されたことを検証している- 1 回も実行されていないのでエラーになる
-
get(0)
が 2 回実行されている場合もエラーになる
引数のマッチング
@Test
void testVerifyWithArgumentMatcher() {
List<String> mock = mock(List.class);
mock.add("one");
mock.add("two");
mock.add("three");
verify(mock).add(startsWith("th")); // テストOK
}
-
verify()
でも、 when() のとき と同じように引数のマッチングができる - 上記実装の場合、
add()
メソッドにth
で始まる値が渡されて実行されていればテストが通る
メソッドが実行された回数を検証する
@Test
void testVerifyInvocationCount() {
List<String> mock = mock(List.class);
mock.get(0);
mock.get(0);
mock.get(0);
verify(mock, times(2)).get(0);
}
list.get(0);
Wanted 2 times:
-> at sandbox.mockito.VerifyTest.testVerifyInvocationCount(VerifyTest.java:39)
But was 3 times:
-> at sandbox.mockito.VerifyTest.testVerifyInvocationCount(VerifyTest.java:35)
-
verify()
の第二引数にMockito#times(int)
を渡すことで、メソッドが実行された回数を検証できる-
times()
が指定されていない場合のデフォルトはtimes(1)
を指定しているのと同じになる
-
- 上記実装の場合、
get(0)
が 2 回呼びだされていることを検証している- 3 回実行されているのでエラーになる
- 0 回や 1 回の実行でもエラーになる
メソッドが最低 n 回実行されていることを検証する
@Test
void testAtLeast() {
List<String> mock = mock(List.class);
mock.get(0);
verify(mock, atLeast(2)).get(0);
}
list.get(0);
Wanted *at least* 2 times:
-> at sandbox.mockito.VerifyTest.testAtLeast(VerifyTest.java:48)
But was 1 time:
-
Mockito#atLeast(int)
を使用すると、メソッドが最低 n 回実行されたことを検証できる - 上記実装では、
get(0)
が最低 2 回実行されていることを検証している- 1 回しか実行されていないのでエラーになる
- 2 回以上実行されていれば OK
-
atLeast(1)
はMockito#atLeastOnce()
というエイリアスのメソッドが用意されている
メソッドが多くても n 回までしか呼び出されていないことを検証する
@Test
void testAtMost() {
List<String> mock = mock(List.class);
mock.get(0);
mock.get(0);
mock.get(0);
verify(mock, atMost(2)).get(0);
}
Wanted at most 2 times but was 3
org.mockito.exceptions.verification.MoreThanAllowedActualInvocations:
Wanted at most 2 times but was 3
-
Mockito#atMost(int)
を使用すると、メソッドが多くても n 回までしか実行されていないことを検証できる - 上記実装では、
get(0)
が多くても 2 回までしか実行されていないことを検証している- 3 回実行されているのでエラーになる
- 0 ~ 2 回の実行であれば OK
-
atMost(1)
はMockito#atMostOnce()
というエイリアスのメソッドが用意されている
メソッドが 1 回も実行されていないことを検証する
@Test
void testNever() {
List<String> mock = mock(List.class);
mock.get(0);
verify(mock, never()).get(0);
}
list.get(0);
Never wanted here:
-> at sandbox.mockito.VerifyTest.testNever(VerifyTest.java:68)
But invoked here:
-> at sandbox.mockito.VerifyTest.testNever(VerifyTest.java:66) with arguments: [0]
-
Mockito#never()
を使用すると、メソッドが 1 回も実行されていないことを検証できる - 上記実装では、
get(0)
が 1 回も実行されていないことを検証している- 1 回実行されているのでエラーになる
メソッドの実行順を検証する
@Test
void testInvocationOrder() {
List<String> mock = mock(List.class);
mock.get(0);
mock.get(2);
mock.get(1);
InOrder inOrder = inOrder(mock);
inOrder.verify(mock).get(0);
inOrder.verify(mock).get(1);
inOrder.verify(mock).get(2);
}
Verification in order failure
Wanted but not invoked:
list.get(2);
-> at sandbox.mockito.VerifyTest.testInvocationOrder(VerifyTest.java:86)
Wanted anywhere AFTER following interaction:
list.get(1);
-> at sandbox.mockito.VerifyTest.testInvocationOrder(VerifyTest.java:80)
-
Mockito#inOrder(モックオブジェクト)
で InOrder を取得し、InOrder
のverify()
を使ってメソッドの呼び出しを検証することで、メソッドの呼び出し順序を検証できる - 上記実装では、
get(0)
->get(1)
->get(2)
の順番でモックのメソッドが実行されていることを検証している- 2 つ目の呼び出しが
get(2)
で異なっているため、エラーになっている
- 2 つ目の呼び出しが
複数のモックに対するメソッドの実行順序を検証する
@Test
void testInvocationOrderWithMultipleMocks() {
List<String> mock1 = mock(List.class);
List<String> mock2 = mock(List.class);
mock1.get(0);
mock1.get(2);
mock2.get(1);
InOrder inOrder = inOrder(mock1, mock2);
inOrder.verify(mock1).get(0);
inOrder.verify(mock2).get(1);
inOrder.verify(mock1).get(2);
}
Verification in order failure
Wanted but not invoked:
list.get(2);
-> at sandbox.mockito.VerifyTest.testInvocationOrderWithMultipleMocks(VerifyTest.java:102)
Wanted anywhere AFTER following interaction:
list.get(1);
-> at sandbox.mockito.VerifyTest.testInvocationOrderWithMultipleMocks(VerifyTest.java:96)
-
inOrder()
の引数は可変長引数になっており、複数のモックオブジェクトを渡すことができる - 返された
InOrder
を使うと、引数で受け取ったモックオブジェクト達に対するメソッドの呼び出し順序を検証できる - 上記実装例では、
mock1.get(0)
->mock2.get(1)
->mock1.get(2)
の順番で呼び出されていることを検証している- 2 つ目の呼び出しが
mock1.get(2)
となっているため、エラーとなっている
- 2 つ目の呼び出しが
実際に渡された引数を取得する
@Test
void testCaptor() {
List<String> mock = mock(List.class);
mock.get(0);
mock.get(1);
mock.get(9);
ArgumentCaptor<Integer> captor = ArgumentCaptor.forClass(Integer.class);
verify(mock, times(3)).get(captor.capture());
System.out.println("captor.getValue() = " + captor.getValue());
System.out.println("captor.getAllValues() = " + captor.getAllValues());
}
captor.getValue() = 9
captor.getAllValues() = [0, 1, 9]
- ArgumentCaptor を使用すると、モックのメソッドに渡された実際の引数を取得(キャプチャ)できる
-
ArgumentCaptor#forClass(Class)
で、キャプチャしたい引数用のArgumentCaptor
を生成する-
Class
は、キャプチャしたい引数の型に合わせる
-
- 生成した
ArgumentCaptor
のcapture()
メソッドを、verify()
による検証の中でキャプチャしたい引数の場所で実行する - キャプチャした結果は、
getValue()
またはgetAllValues()
で取得できる-
getValue()
は最後にキャプチャした値を返す -
getAllValues()
は、メソッドが複数回呼ばれた場合にそれぞれの呼び出しで渡された引数の値をリストで取得できる- メソッドが呼び出された順番でリストに設定されている
-
スパイ
package sandbox.mockito;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
public class SpyTest {
@Test
void test() {
Hoge hoge = new Hoge();
Hoge spy = spy(hoge);
when(spy.size()).thenReturn(100);
spy.add("hello");
spy.add("world");
spy.setName("Spied!");
System.out.println("spy = " + spy);
System.out.println("spy.size = " + spy.size());
System.out.println("hoge = " + hoge);
System.out.println("hoge.size = " + hoge.size());
}
public static class Hoge implements Cloneable {
private List<String> list = new ArrayList<>();
private String name;
public void setName(String name) {
this.name = name;
}
public void add(String value) {
list.add(value);
}
public int size() {
return list.size();
}
@Override
public String toString() {
return "Hoge{" + "list=" + list + ", name='" + name + '\'' + '}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
}
}
spy = Hoge{list=[hello, world], name='Spied!'}
spy.size = 100
hoge = Hoge{list=[hello, world], name='null'}
hoge.size = 2
-
spy(T)
メソッドに任意のオブジェクトを渡すことで、スパイを作成できる - スパイのメソッドは、スタブ化されていない場合は実際のメソッドの処理を実行する
- 上の実装例では、スタブ化している
size()
以外は実際のメソッドが実行されている - 一部のメソッドだけをスタブ化したい場合に利用できる
- 上の実装例では、スタブ化している
- スパイはオリジナルのオブジェクトのコピーを元に作成されている
- スパイ自体の状態を変更しても、オリジナルのオブジェクトの状態は変更されない
- 上の実装例では、
name
の状態は別々になっている
- 上の実装例では、
- ただしディープコピーではないので、別オブジェクトへの参照を持つ場合は共有される
- 上の実装例では、
list
の状態が共有されている
- 上の実装例では、
- コピーに
clone()
は使われていない- もし
clone()
でコピーしていたら、上の実装は落ちるはず
- もし
- スパイ自体の状態を変更しても、オリジナルのオブジェクトの状態は変更されない
実際のメソッドを呼ばずにスタブ化する
先に失敗例。
@Test
void testStubbingSpyWithError() {
List<String> list = new ArrayList<>();
List<String> spy = spy(list);
when(spy.get(0)).thenReturn("spied");
System.out.println("spy.get(0) = " + spy.get(0));
}
-
List
のスパイを作成し、get(0)
の結果をスタブ化しようとしている
java.lang.IndexOutOfBoundsException: Index 0 out of bounds for length 0
-
IndexOutOfBoundsException
がスローされる-
when(spy.get(0))
の時点では、まだget(0)
はスタブ化されていない - このため、
get(0)
は実際のメソッドが実行されてしまい、IndexOutOfBoundsException
がスローされてしまっている
-
- このように、スパイのメソッドをスタブ化しようとすると、
when(<メソッド呼び出し>)
だと実際のメソッドが実行されてしまい不都合が発生することがある - この問題は、以下のように
doReturn()
を使うことで回避できる
@Test
void testStubbingSpyNoError() {
List<String> list = new ArrayList<>();
List<String> spy = spy(list);
doReturn("spied").when(spy).get(0);
System.out.println("spy.get(0) = " + spy.get(0));
}
spy.get(0) = spied
-
doReturn(<戻り値>)
で始めて、続けてwhen(<スパイオブジェクト>)
とする -
when()
の戻り値の型は引数で渡したスパイオブジェクトの型になっているので、そのままスタブ化したいメソッドを呼び出す-
ArgumtnetMatchers
を使って引数のマッチ条件を指定することも可能
-
- こうするとスパイオブジェクト自体のメソッドを実行することなくスタブを定義できるため、先の問題が回避できる
- スパイのメソッドを実行せずに例外をスローするようにスタブ化させたい場合は、
doThrow()
を使用する
doReturn().when().xxx()
の方法の場合、 doReturn()
の引数に渡す値の型と最後に呼び出すスタブ化したいメソッドの戻り値の型は一致していなくてもコンパイルエラーにならない。
List<String> mock = mock(List.class);
doReturn(10).when(mock).get(0); // get() の戻り値は String なのに、 doRetrun() には int を渡せる
この場合、実行時に ClassCastException
がスローされてエラーになる。
このため、スパイにあるような特別な理由がない限りは、 doReturn().when().xxx()
よりも when(mock.xxx()).doReturn()
の書き方を使うことが推奨されている。
抽象クラスのスパイ
package sandbox.mockito;
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.spy;
public class AbstractSpyTest {
@Test
void test() {
AbstractClass mock = spy(AbstractClass.class);
System.out.println("mock.concreteMethod() = " + mock.concreteMethod());
System.out.println("mock.abstractStringMethod() = " + mock.abstractStringMethod());
}
public static abstract class AbstractClass {
public String concreteMethod() {
return "concreteMethod";
}
abstract public String abstractStringMethod();
}
}
mock.concreteMethod() = concreteMethod
mock.abstractStringMethod() = null
- 通常のスパイの生成には元となるインスタンスが必要だが、
Class
オブジェクトを受け取るspy(Class)
を使うと元のインスタンス無しでスパイを生成できる - 抽象クラスのように、元のインスタンス生成が面倒なクラスのスパイを用意するときに利用できる
- 抽象メソッドの戻り値は、モックオブジェクトのデフォルトの戻り値と同じ値が返される
- スパイ対象のクラスにはデフォルトコンストラクターが必要
本物のオブジェクトに委譲する形でモック(スパイ)化する
package sandbox.mockito;
import org.junit.jupiter.api.Test;
import static org.mockito.AdditionalAnswers.delegatesTo;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class DelegateTest {
@Test
void test() {
HogeFunction mock = mock(HogeFunction.class, delegatesTo(new FinalClass()));
when(mock.world()).thenReturn("mocked");
System.out.println(mock.hello(10));
System.out.println(mock.world());
}
public interface HogeFunction {
String hello(int n);
String world();
}
public final static class FinalClass {
public String hello() {
return "hello()";
}
public String hello(int n) {
return "hello(" + n + ")";
}
public String world() {
return "world()";
}
}
}
hello(10)
mocked
-
mock(Class, AdditionalAnswers.delegatesTo(<委譲先オブジェクト>))
とすることで、各メソッドの処理を委譲先オブジェクトに委譲するモックを作成できる - 委譲先のオブジェクトがモックの型を継承している必要はない
- シグネチャが一致するメソッドに委譲される
- 委譲先のメソッドがない場合は実行時エラーになる
- これは、以下のようなオブジェクトやメソッドをモック/スタブ化したい場合に役立つ
-
final
なクラスやメソッド- mockito は、標準では
final
なクラス/メソッドはモック/スタブ化できない1
- mockito は、標準では
- すでにプロキシされているオブジェクト
- Spring の Bean とかを指している?
-
モックをシリアライズ可能にする
package sandbox.mockito;
import org.junit.jupiter.api.Test;
import java.io.Serializable;
import java.util.List;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.withSettings;
public class SerializableTest {
@Test
void test() {
List<String> noSerializableMock = mock(List.class);
System.out.println(noSerializableMock instanceof Serializable);
List<String> serializableMock = mock(List.class, withSettings().serializable());
System.out.println(serializableMock instanceof Serializable);
}
}
false
true
-
mock()
メソッドの第二引数にMockito.withSettings().serializable()
を渡すと、生成されたモックはシリアライズ可能になる
ワンライナーでモックを生成してスタブを定義する
package sandbox.mockito;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class OneLinerTest {
List<String> mock = when(mock(List.class).get(0)).thenReturn("mocked").getMock();
@Test
void test() {
System.out.println("mock.get(0) = " + mock.get(0));
}
}
mock.get(0) = mocked
-
thenReturn()
/thenThrow()
の後にgetMock()
でモックオブジェクトを取得できるので、モックの生成からスタブの定義までワンライナーで書ける
アノテーションで定義する
package sandbox.mockito;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.*;
import java.util.ArrayList;
import java.util.List;
import static org.mockito.Mockito.*;
public class AnnotationTest {
@Mock List<String> mock;
@Spy List<String> spy = new ArrayList<>();
@Captor ArgumentCaptor<String> captor;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
void test() {
when(mock.get(0)).thenReturn("mocked");
System.out.println("mock.get(0) = " + mock.get(0));
spy.add("hello");
spy.add("world");
doReturn("spied").when(spy).get(0);
System.out.println("spy.get(0) = " + spy.get(0));
System.out.println("spy.get(1) = " + spy.get(1));
verify(spy, atLeastOnce()).add(captor.capture());
System.out.println("captor.getAllValues() = " + captor.getAllValues());
}
}
mock.get(0) = mocked
spy.get(0) = spied
spy.get(1) = world
captor.getAllValues() = [hello, world]
-
MockitoAnnotations#openMocks(<テストクラスのインスタンス>)
を実行すると、アノテーションを使ってモックやスパイを定義できるようになる - テストクラスに宣言したフィールドにアノテーションをつけることで定義できる
-
@Mock
でモックを定義できる -
@Spy
でスパイを定義できる- フィールドにオブジェクトが設定されている場合は、そのオブジェクトを元にスパイがつくられる
- フィールドに何も設定されていない場合は、デフォルトコンストラクタで生成されたオブジェクトが使用される
- たぶん、
spy(Class)
を使った場合と同じ振る舞い
- たぶん、
-
@Captor
でArgumentCaptor
を定義できる
-
JUnit 5 用の拡張
dependencies {
// testImplementation "org.mockito:mockito-core:4.6.1"
testImplementation "org.mockito:mockito-junit-jupiter:4.6.1"
testImplementation "org.junit.jupiter:junit-jupiter:5.8.2"
}
-
mockit-core
の代わりに org.mockito:mockito-junit-jupiter を依存関係に設定する
package sandbox.mockito;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import java.util.Map;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class JUnit5ExtensionTest {
@Mock List<String> mockList;
@Test
void test() {
when(mockList.get(0)).thenReturn("mocked");
System.out.println("mockList.get(0) = " + mockList.get(0));
}
@Test
void testMethodArgument(@Mock Map<String, String> mockMap) {
when(mockMap.get("hello")).thenReturn("mocked");
System.out.println("mockMap.get(hello) = " + mockMap.get("hello"));
}
}
mockList.get(0) = mocked
mockMap.get(hello) = mocked
-
mockito-junit-jupiter
を依存関係に追加すると、 MockitoExtension という JUnit 5 用の拡張クラスが使えるようになる -
MockitoExtension
を使用すると、MockitoAnnotations#openMocks(Object)
を自動でやってくれるようになる - また、メソッド引数でモックを受け取ることもできるようになる
inline mock maker
inline mock maker という拡張を導入すると、通常の mockito では実現できない final クラスのモック化や static メソッドのスタブ化などができるようになる。
inline mock maker は、 org.mockito:mockit-inline の依存関係を設定するだけで使えるようになる。
dependencies {
// testImplementation "org.mockito:mockito-core:4.6.1"
testImplementation "org.mockito:mockito-inline:4.6.1"
testImplementation "org.junit.jupiter:junit-jupiter:5.8.2"
}
final クラス/メソッドをモック/スタブ化する
package sandbox.mockito;
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class MockFinalClassTest {
@Test
void test() {
FinalClass mock = mock(FinalClass.class);
when(mock.hello()).thenReturn("mocked");
System.out.println("mock.hello() = " + mock.hello());
}
public static final class FinalClass {
public String hello() {
return "world";
}
}
}
mock.hello() = mocked
- inline mock maker を導入すると、それだけで
final
クラスやメソッドをモック/スタブ化できるようになる
static クラス/メソッドをモック/スタブ化する
package sandbox.mockito;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
public class StaticMethodTest {
@Test
void test() {
System.out.println("Hoge.hello() = " + Hoge.hello("before"));
try (MockedStatic<Hoge> mocked = Mockito.mockStatic(Hoge.class)) {
mocked.when(() -> Hoge.hello("test")).thenReturn("mocked");
System.out.println("Hoge.hello() = " + Hoge.hello("test"));
mocked.verify(() -> Hoge.hello("test"));
}
System.out.println("Hoge.hello() = " + Hoge.hello("after"));
}
public static class Hoge {
public static String hello(String arg) {
return "world (arg=" + arg + ")";
}
}
}
Hoge.hello() = world (arg=before)
Hoge.hello() = mocked
Hoge.hello() = world (arg=after)
-
Mockito#mockStatic(Class)
で、スタブ化したいstatic
メソッドを持つクラスを指定する -
static
メソッドのスタブ化が有効なのは、mockStatic()
の戻り値であるMockedStatic
がclose()
されるまでの間だけ- try-with-resources 文で確実に
close()
されるようにすることが推奨されている
- try-with-resources 文で確実に
-
static
メソッドのスタブは、MockedStatic
のwhen(Verification)
メソッドで定義する- 引数にはスタブ化したい
static
メソッドの呼び出しを実装したVerification
を渡す -
Verification
は関数型インタフェースなので、ラムダ式でスタブ化したいメソッドを呼び出すように書くのが楽- 引数がないメソッドであれば、メソッド参照で書くことも可能(
mocked.when(SomeClass::staticMethod)
)
- 引数がないメソッドであれば、メソッド参照で書くことも可能(
- 戻り値が
void
のメソッドも、同じようにwhen()
でスタブ化する-
mocked.doReturn(...).when(...)
のような書き方はない
-
- 引数にはスタブ化したい
-
verify()
も、Mocked
に用意されているメソッドを使って検証する
コンストラクタがモックを返すようにする
package sandbox.mockito;
import org.junit.jupiter.api.Test;
import org.mockito.MockedConstruction;
import static org.mockito.Mockito.mockConstruction;
import static org.mockito.Mockito.when;
public class MockConstructorTest {
@Test
void test() {
System.out.println("[before] new Hoge().hello() = " + new Hoge().hello());
try (MockedConstruction<Hoge> mocked = mockConstruction(Hoge.class)) {
Hoge mock = new Hoge();
when(mock.hello()).thenReturn("mocked");
System.out.println("mock.hello() = " + mock.hello());
System.out.println("new Hoge().hello() = " + new Hoge().hello());
}
System.out.println("[after] new Hoge().hello() = " + new Hoge().hello());
}
public static class Hoge {
public String hello() {
return "world";
}
}
}
[before] new Hoge().hello() = world
mock.hello() = mocked
new Hoge().hello() = null
[after] new Hoge().hello() = world
-
Mockito#mockConstrcution(Class)
で、Class
オブジェクトを渡したクラスのコンストラクタはモックオブジェクトを返すようになる - コンストラクタがモックを返すのは、
mockConstrcution()
の戻り値であるMockedConstruction
がclose()
されるまでの間だけ- try-with-resources 文で確実に
close()
させることが推奨されている
- try-with-resources 文で確実に
- スタブ化や検証は、通常の Mockito の利用方法と同じで
when()
,verify()
を使う
モックを初期化する
@Test
public void test2() {
try (final MockedConstruction<Hoge> mocked = mockConstruction(Hoge.class, (mock, ctx) -> {
when(mock.hello()).thenReturn("mocked");
})) {
final Hoge hoge = new Hoge();
System.out.println(hoge.hello());
}
}
mocked
- よそのメソッド内部などで
new
で生成されるモックオブジェクトに対して初期化を行いたい場合は、mockConstruction
の第二引数にMockInitializer
を渡す - 第一引数に外部で生成されたモックオブジェクトが渡されるので、他と同様の方法で操作を記録することができる
モック化できないクラスを定義する
package sandbox.mockito.org.mockito;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DoNotMock {
}
-
DoNotMock
という名前のアノテーションを、<任意のパッケージ>.org.mockito
パッケージの下に作成する -
@Target
をTYPE
にして、@Retention
をRUNTIME
にする
package sandbox.mockito;
import org.junit.jupiter.api.Test;
import sandbox.mockito.org.mockito.DoNotMock;
import static org.mockito.Mockito.mock;
public class DoNotMockTest {
@Test
void test() {
mock(Hoge.class);
}
@DoNotMock
public static class Hoge {}
}
-
Hoge
クラスに@DoNotMock
アノテーションを設定し、モック化を試みる
org.mockito.exceptions.misusing.DoNotMockException: class sandbox.mockito.DoNotMockTest$Hoge is annotated with @org.mockito.DoNotMock and can't be mocked.
- モック化に失敗する
-
<任意のパッケージ>.org.mockito.DoNotMock
という名前のアノテーションを作成してクラスに設定しておくと、そのクラスのモック化を禁止できる
参考
-
後述する inline mock maker という拡張機能を使用すると可能 ↩