Mockito vs EasyMock vs JMockit
1. 前書き
1.1. 概要
この投稿では、mockingについて説明します。それは何であるか、なぜそれを使用するのか、そしてJavaで最もよく使用されるモックライブラリのいくつかを使用して同じテストケースをモックする方法のいくつかの例です。
モックの概念のいくつかの正式/準正式な定義から始めます。次に、テスト中のケースを提示し、各ライブラリの例をフォローアップして、いくつかの結論を出します。 選択されたライブラリは、Mockito、EasyMock、およびJMockitです。
モックの基本をすでに理解していると感じたら、次の3つのポイントを読まずにポイント2にスキップできます。
1.2. モックを使用する理由
テストを中心としたいくつかの駆動開発方法論(TDD、ATDD、またはBDD)に従って、すでにコーディングを行っていることを前提としています。 または、機能を実現するために依存関係に依存する既存のクラスのテストを作成することもできます。
いずれにせよ、クラスの単体テストを行うときは、test only its functionality and not that of its dependenciesにします(実装を信頼しているため、または自分でテストするため)。
これを実現するには、テスト対象オブジェクトに、その依存関係を制御できる置換を提供する必要があります。 このようにして、極端な戻り値、例外のスローを強制したり、単に時間のかかるメソッドを固定の戻り値に減らしたりできます。
この制御された置換はmockであり、テストコーディングを簡素化し、テスト実行時間を短縮するのに役立ちます。
1.3. モックの概念と定義
Martin Fowlerによって書かれたarticleからの4つの定義を見てみましょう。これは、モックについて誰もが知っておくべき基本事項をまとめたものです。
-
Dummyオブジェクトは渡されますが、実際には使用されません。 通常、それらは単にパラメーターリストを埋めるために使用されます。
-
Fakeオブジェクトには機能する実装がありますが、通常は、本番環境に適さないようにするショートカットを使用します(インメモリデータベースが良い例です)。
-
Stubsは、テスト中に行われた呼び出しに対して返信定型文を提供します。通常、テスト用にプログラムされたもの以外にはまったく応答しません。 スタブは、「送信した」メッセージを記憶する電子メールゲートウェイスタブ、または「送信した」メッセージの数だけなど、コールに関する情報も記録する場合があります。
-
Mocksは、ここで話していることです。オブジェクトは、受信することが期待される呼び出しの仕様を形成する期待値で事前にプログラムされています。
1.4 To Mock or Not to Mock: That Is the Question
Not everything must be mocked。 そのメソッド/機能をあざけるだけでは実際のメリットはほとんどないため、統合テストを実行する方がよい場合があります。 私たちのテストケース(次のポイントで示されます)では、LoginDaoをテストします。
LoginDaoは、DBアクセスにサードパーティのライブラリを使用します。それをあざけるのは、呼び出し用にパラメータが準備されていることを確認することだけですが、呼び出しが必要なデータを返すことをテストする必要があります。
そのため、この例には含まれません(ただし、サードパーティライブラリ呼び出しのモック呼び出しを使用した単体テストと、サードパーティライブラリの実際のパフォーマンスをテストするためのDBUnitとの統合テストの両方を記述できます)。
2. テストケース
前のセクションのすべてを念頭に置いて、非常に典型的なテストケースと、モックを使用してテストする方法(モックを使用することが理にかなっている場合)を提案しましょう。 これは、後でさまざまなモックライブラリを比較できるようにするための一般的なシナリオを作成するのに役立ちます。
2.1 Proposed Case
提案されたテストケースは、階層化アーキテクチャを備えたアプリケーションでのログインプロセスです。
ログイン要求は、DAO(DBでユーザー資格情報を検索する)を使用するサービスを使用するコントローラーによって処理されます。 各レイヤーの実装についてはあまり深く掘り下げず、各レイヤーのinteractions between the componentsに焦点を当てます。
このようにして、LoginController、LoginService、およびLoginDAOが作成されます。 明確にするために図を見てみましょう:
2.2 Implementation
次に、テストケースに使用される実装について説明します。これにより、テストで何が起こっているのか(または何が起こるべきか)を理解できます。
まず、すべての操作に使用されるモデルUserFormから始めます。このモデルは、ユーザーの名前とパスワード(単純化するためにパブリックアクセス修飾子を使用しています)とusernameフィールドのgetterメソッドのみを保持しますそのプロパティのモックを許可するには:
public class UserForm {
public String password;
public String username;
public String getUsername(){
return username;
}
}
LoginDAOを続けましょう。必要なときにそれらをモックできるように、メソッドだけが存在するようにしたいので、機能は無効になります。
public class LoginDao {
public int login(UserForm userForm){
return 0;
}
}
LoginDaoは、loginメソッドのLoginServiceによって使用されます。 LoginServiceには、そのモックをテストするためにvoidを返すsetCurrentUserメソッドもあります。
public class LoginService {
private LoginDao loginDao;
private String currentUser;
public boolean login(UserForm userForm) {
assert null != userForm;
int loginResults = loginDao.login(userForm);
switch (loginResults){
case 1:
return true;
default:
return false;
}
}
public void setCurrentUser(String username) {
if(null != username){
this.currentUser = username;
}
}
}
最後に、LoginControllerはloginメソッドにLoginServiceを使用します。 これには以下が含まれます。
-
模擬サービスへの呼び出しが行われない場合。
-
1つのメソッドのみが呼び出されるケース。
-
すべてのメソッドが呼び出されるケース。
-
例外のスローがテストされるケース。
public class LoginController {
public LoginService loginService;
public String login(UserForm userForm){
if(null == userForm){
return "ERROR";
}else{
boolean logged;
try {
logged = loginService.login(userForm);
} catch (Exception e) {
return "ERROR";
}
if(logged){
loginService.setCurrentUser(userForm.getUsername());
return "OK";
}else{
return "KO";
}
}
}
}
テストしようとしていることを確認したので、各ライブラリでそれをどのようにモックするかを見てみましょう。
3. テスト設定
3.1 Mockito
Mockitoの場合、バージョン2.8.9を使用します。
モックを作成して使用する最も簡単な方法は、@Mockおよび@InjectMocksアノテーションを使用することです。 最初のものは、フィールドを定義するために使用されるクラスのモックを作成し、2つ目は、作成されたモックを注釈付きモックに注入しようとします。
部分的なモック(モックされていないメソッドで通常の実装を使用するモック)を作成できる@Spyなどのアノテーションがさらにあります。
そうは言っても、この「魔法」のすべてが機能するために前述のモックを使用するテストを実行する前に、MockitoAnnotations.initMocks(this)を呼び出す必要があります。 これは通常、@Beforeアノテーション付きメソッドで実行されます。 MockitoJUnitRunnerを使用することもできます。
public class LoginControllerTest {
@Mock
private LoginDao loginDao;
@Spy
@InjectMocks
private LoginService spiedLoginService;
@Mock
private LoginService loginService;
@InjectMocks
private LoginController loginController;
@Before
public void setUp() {
loginController = new LoginController();
MockitoAnnotations.initMocks(this);
}
}
3.2 EasyMock
EasyMockの場合、バージョン3.4(Javadoc)を使用します。 EasyMockでは、モックが「機能」を開始するには、すべてのテストメソッドでEasyMock.replay(mock)を呼び出す必要があります。そうしないと、例外が発生します。
モックとテスト済みクラスはアノテーションを介して定義することもできますが、この場合、静的メソッドを呼び出す代わりに、テストクラスにEasyMockRunnerを使用します。
モックは@Mockアノテーションで作成され、テストされたオブジェクトは@TestSubjectアノテーションで作成されます(作成されたモックから依存関係が注入されます)。 テストするオブジェクトはインラインで作成する必要があります。
@RunWith(EasyMockRunner.class)
public class LoginControllerTest {
@Mock
private LoginDao loginDao;
@Mock
private LoginService loginService;
@TestSubject
private LoginController loginController = new LoginController();
}
3.3. JMockit
JMockitのセットアップは、部分的なモックに特定の注釈がなく(実際には必要ない)、テストランナーとしてJMockitを使用する必要があることを除いて、Mockitoと同じくらい簡単です。
モックは、@Injectableアノテーション(1つのモックインスタンスのみを作成する)または@Mockedアノテーション(アノテーション付きフィールドのクラスのすべてのインスタンスに対してモックを作成する)を使用して定義されます。
テストされたインスタンスは、@Testedアノテーションを使用して作成されます(およびそのモックされた依存関係が注入されます)。
@RunWith(JMockit.class)
public class LoginControllerTest {
@Injectable
private LoginDao loginDao;
@Injectable
private LoginService loginService;
@Tested
private LoginController loginController;
}
4. モックへの呼び出しがないことの確認
4.1. モッキート
モックがMockitoで呼び出しを受信しなかったことを確認するために、モックを受け入れるメソッドverifyZeroInteractions()があります。
@Test
public void assertThatNoMethodHasBeenCalled() {
loginController.login(null);
Mockito.verifyZeroInteractions(loginService);
}
4.2. EasyMock
モックが呼び出しを受信しなかったことを確認するには、動作を指定しないだけで、モックを再生し、最後に確認します。
@Test
public void assertThatNoMethodHasBeenCalled() {
EasyMock.replay(loginService);
loginController.login(null);
EasyMock.verify(loginService);
}
4.3. JMockit
モックが呼び出しを受信しなかったことを確認するには、そのモックの期待値を指定せず、そのモックに対してFullVerifications(mock)を実行します。
@Test
public void assertThatNoMethodHasBeenCalled() {
loginController.login(null);
new FullVerifications(loginService) {};
}
5. モックされたメソッド呼び出しの定義とモックへの呼び出しの検証
5.1. モッキート
mocking method callsには、Mockito.when(mock.method(args)).thenReturn(value)を使用できます。 ここでは、パラメータとして追加するだけで、複数の呼び出しに対して異なる値を返すことができます:thenReturn(value1, value2, value-n, …)。
この構文では、voidを返すメソッドをモックすることはできないことに注意してください。 上記の場合、上記の方法の検証を使用します(11行目に示されています)。
モックへのverifying callsの場合、Mockito.verify(mock).method(args)を使用できます。また、verifyNoMoreInteractions(mock)を使用してモックへの呼び出しがこれ以上行われなかったことを確認することもできます。
verifying argsの場合、特定の値を渡すか、any()、anyString()、anyInt().などの事前定義されたマッチャーを使用できます。この種のマッチャーは他にもたくさんあり、定義することもできます。次の例で見るマッチャー。
@Test
public void assertTwoMethodsHaveBeenCalled() {
UserForm userForm = new UserForm();
userForm.username = "foo";
Mockito.when(loginService.login(userForm)).thenReturn(true);
String login = loginController.login(userForm);
Assert.assertEquals("OK", login);
Mockito.verify(loginService).login(userForm);
Mockito.verify(loginService).setCurrentUser("foo");
}
@Test
public void assertOnlyOneMethodHasBeenCalled() {
UserForm userForm = new UserForm();
userForm.username = "foo";
Mockito.when(loginService.login(userForm)).thenReturn(false);
String login = loginController.login(userForm);
Assert.assertEquals("KO", login);
Mockito.verify(loginService).login(userForm);
Mockito.verifyNoMoreInteractions(loginService);
}
5.2. EasyMock
mocking method callsには、EasyMock.expect(mock.method(args)).andReturn(value)を使用します。
モックへのverifying callsの場合、EasyMock.verify(mock)を使用できますが、EasyMock.replay(mock)を呼び出すalways afterと呼ぶ必要があります。
verifying argsの場合、特定の値を渡すことができます。または、その種類のマッチャーのisA(Class.class)、anyString()、anyInt()、lot moreなどの事前定義されたマッチャーがあります。そして再びあなたのマッチャーを定義する可能性。
@Test
public void assertTwoMethodsHaveBeenCalled() {
UserForm userForm = new UserForm();
userForm.username = "foo";
EasyMock.expect(loginService.login(userForm)).andReturn(true);
loginService.setCurrentUser("foo");
EasyMock.replay(loginService);
String login = loginController.login(userForm);
Assert.assertEquals("OK", login);
EasyMock.verify(loginService);
}
@Test
public void assertOnlyOneMethodHasBeenCalled() {
UserForm userForm = new UserForm();
userForm.username = "foo";
EasyMock.expect(loginService.login(userForm)).andReturn(false);
EasyMock.replay(loginService);
String login = loginController.login(userForm);
Assert.assertEquals("KO", login);
EasyMock.verify(loginService);
}
5.3. JMockit
JMockitを使用して、テスト用にstepsを定義しました:記録、再生、検証。
Recordは新しいExpectations()\{\{}}ブロック(複数のモックのアクションを定義できます)で実行され、replayはテストされたクラスのメソッドを呼び出すだけで実行されます(モックを呼び出す必要があります)オブジェクト)、およびverificationは、新しいVerifications()\{\{}}ブロック(複数のモックの検証を定義できる)内で実行されます。
mocking method callsの場合、任意のExpectationsブロック内でmock.method(args); result = value;を使用できます。 ここでは、result = value;の代わりにreturns(value1, value2, …, valuen);を使用するだけで、複数の呼び出しに対して異なる値を返すことができます。
モックへのverifying callsの場合、新しい検証()\{\{mock.call(value)}}またはnew Verifications(mock)\{\{}}を使用して、以前に定義されたすべての予想される呼び出しを検証できます。
verifying argsの場合、特定の値を渡すことができます。または、any、anyString、anyLongなどのpredefined values、およびそのような種類の特別な値が多数あります。また、マッチャーを定義する可能性もあります(Hamcrestマッチャーである必要があります)。
@Test
public void assertTwoMethodsHaveBeenCalled() {
UserForm userForm = new UserForm();
userForm.username = "foo";
new Expectations() {{
loginService.login(userForm); result = true;
loginService.setCurrentUser("foo");
}};
String login = loginController.login(userForm);
Assert.assertEquals("OK", login);
new FullVerifications(loginService) {};
}
@Test
public void assertOnlyOneMethodHasBeenCalled() {
UserForm userForm = new UserForm();
userForm.username = "foo";
new Expectations() {{
loginService.login(userForm); result = false;
// no expectation for setCurrentUser
}};
String login = loginController.login(userForm);
Assert.assertEquals("KO", login);
new FullVerifications(loginService) {};
}
6. 例外スローをあざける
6.1. モッキート
例外スローは、Mockito.when(mock.method(args))の後に.thenThrow(ExceptionClass.class)を使用してモックすることができます。
@Test
public void mockExceptionThrowin() {
UserForm userForm = new UserForm();
Mockito.when(loginService.login(userForm)).thenThrow(IllegalArgumentException.class);
String login = loginController.login(userForm);
Assert.assertEquals("ERROR", login);
Mockito.verify(loginService).login(userForm);
Mockito.verifyZeroInteractions(loginService);
}
6.2. EasyMock
例外スローは、EasyMock.expect(…)呼び出しの後に.andThrow(new ExceptionClass())を使用してモックすることができます。
@Test
public void mockExceptionThrowing() {
UserForm userForm = new UserForm();
EasyMock.expect(loginService.login(userForm)).andThrow(new IllegalArgumentException());
EasyMock.replay(loginService);
String login = loginController.login(userForm);
Assert.assertEquals("ERROR", login);
EasyMock.verify(loginService);
}
6.3. JMockit
JMockitoを使用した例外スローのモックは特に簡単です。 「通常」の戻り値の代わりに、模擬メソッド呼び出しの結果として例外を返します。
@Test
public void mockExceptionThrowing() {
UserForm userForm = new UserForm();
new Expectations() {{
loginService.login(userForm); result = new IllegalArgumentException();
// no expectation for setCurrentUser
}};
String login = loginController.login(userForm);
Assert.assertEquals("ERROR", login);
new FullVerifications(loginService) {};
}
7. 渡すオブジェクトをモックする
7.1. モッキート
モックを作成して、メソッド呼び出しの引数として渡すこともできます。 Mockitoを使用すると、ワンライナーでそれを行うことができます。
@Test
public void mockAnObjectToPassAround() {
UserForm userForm = Mockito.when(Mockito.mock(UserForm.class).getUsername())
.thenReturn("foo").getMock();
Mockito.when(loginService.login(userForm)).thenReturn(true);
String login = loginController.login(userForm);
Assert.assertEquals("OK", login);
Mockito.verify(loginService).login(userForm);
Mockito.verify(loginService).setCurrentUser("foo");
}
7.2. EasyMock
モックはEasyMock.mock(Class.class)とインラインで作成できます。 その後、EasyMock.expect(mock.method())を使用して実行の準備をすることができます。常に、使用する前にEasyMock.replay(mock)を呼び出すことを忘れないでください。
@Test
public void mockAnObjectToPassAround() {
UserForm userForm = EasyMock.mock(UserForm.class);
EasyMock.expect(userForm.getUsername()).andReturn("foo");
EasyMock.expect(loginService.login(userForm)).andReturn(true);
loginService.setCurrentUser("foo");
EasyMock.replay(userForm);
EasyMock.replay(loginService);
String login = loginController.login(userForm);
Assert.assertEquals("OK", login);
EasyMock.verify(userForm);
EasyMock.verify(loginService);
}
7.3. JMockit
1つのメソッドだけのオブジェクトをモックするには、単純にオブジェクトをテストメソッドのパラメーターとしてモックします。 その後、他のモックと同様に期待を作成できます。
@Test
public void mockAnObjectToPassAround(@Mocked UserForm userForm) {
new Expectations() {{
userForm.getUsername(); result = "foo";
loginService.login(userForm); result = true;
loginService.setCurrentUser("foo");
}};
String login = loginController.login(userForm);
Assert.assertEquals("OK", login);
new FullVerifications(loginService) {};
new FullVerifications(userForm) {};
}
8. カスタム引数マッチング
8.1. モッキート
モックされた呼び出しの引数マッチングは、固定値またはanyString()よりも少し複雑にする必要がある場合があります。 その場合、Mockitoには、argThat(ArgumentMatcher<>)で使用されるマッチャークラスがあります。
@Test
public void argumentMatching() {
UserForm userForm = new UserForm();
userForm.username = "foo";
// default matcher
Mockito.when(loginService.login(Mockito.any(UserForm.class))).thenReturn(true);
String login = loginController.login(userForm);
Assert.assertEquals("OK", login);
Mockito.verify(loginService).login(userForm);
// complex matcher
Mockito.verify(loginService).setCurrentUser(ArgumentMatchers.argThat(
new ArgumentMatcher() {
@Override
public boolean matches(String argument) {
return argument.startsWith("foo");
}
}
));
}
8.2. EasyMock
EasyMockでは、実際のマッチャーを作成してからEasyMock.reportMatcher(IArgumentMatcher)でレポートする静的メソッドを作成する必要があるため、カスタム引数のマッチングは少し複雑です。
このメソッドを作成したら、メソッドの呼び出しを使用してモックの期待値で使用します(行の例で見られるように)。
@Test
public void argumentMatching() {
UserForm userForm = new UserForm();
userForm.username = "foo";
// default matcher
EasyMock.expect(loginService.login(EasyMock.isA(UserForm.class))).andReturn(true);
// complex matcher
loginService.setCurrentUser(specificArgumentMatching("foo"));
EasyMock.replay(loginService);
String login = loginController.login(userForm);
Assert.assertEquals("OK", login);
EasyMock.verify(loginService);
}
private static String specificArgumentMatching(String expected) {
EasyMock.reportMatcher(new IArgumentMatcher() {
@Override
public boolean matches(Object argument) {
return argument instanceof String
&& ((String) argument).startsWith(expected);
}
@Override
public void appendTo(StringBuffer buffer) {
//NOOP
}
});
return null;
}
8.3. JMockit
JMockitとのカスタム引数の照合は、特別なwithArgThat(Matcher)メソッド(HamcrestのMatcherオブジェクトを受け取る)を使用して行われます。
@Test
public void argumentMatching() {
UserForm userForm = new UserForm();
userForm.username = "foo";
// default matcher
new Expectations() {{
loginService.login((UserForm) any);
result = true;
// complex matcher
loginService.setCurrentUser(withArgThat(new BaseMatcher() {
@Override
public boolean matches(Object item) {
return item instanceof String && ((String) item).startsWith("foo");
}
@Override
public void describeTo(Description description) {
//NOOP
}
}));
}};
String login = loginController.login(userForm);
Assert.assertEquals("OK", login);
new FullVerifications(loginService) {};
}
9. 部分的なモック
9.1. モッキート
Mockitoは、2つの方法で部分的なモック(一部のメソッドでモックされたメソッド呼び出しの代わりに実際の実装を使用するモック)を許可します。
通常のモックメソッド呼び出し定義で.thenCallRealMethod()を使用するか、モックの代わりにspyを作成することができます。その場合、デフォルトの動作は、すべての非モックメソッド。
@Test
public void partialMocking() {
// use partial mock
loginController.loginService = spiedLoginService;
UserForm userForm = new UserForm();
userForm.username = "foo";
// let service's login use implementation so let's mock DAO call
Mockito.when(loginDao.login(userForm)).thenReturn(1);
String login = loginController.login(userForm);
Assert.assertEquals("OK", login);
// verify mocked call
Mockito.verify(spiedLoginService).setCurrentUser("foo");
}
9.2. EasyMock
また、EasyMockでは、モックを作成するときにどのメソッドをモックするかを定義する必要があるため、部分モックはもう少し複雑になります。
これはEasyMock.partialMockBuilder(Class.class).addMockedMethod(“methodName”).createMock()で行われます。 これが完了すると、モックを他の非部分的なモックとして使用できます。
@Test
public void partialMocking() {
UserForm userForm = new UserForm();
userForm.username = "foo";
// use partial mock
LoginService loginServicePartial = EasyMock.partialMockBuilder(LoginService.class)
.addMockedMethod("setCurrentUser").createMock();
loginServicePartial.setCurrentUser("foo");
// let service's login use implementation so let's mock DAO call
EasyMock.expect(loginDao.login(userForm)).andReturn(1);
loginServicePartial.setLoginDao(loginDao);
loginController.loginService = loginServicePartial;
EasyMock.replay(loginDao);
EasyMock.replay(loginServicePartial);
String login = loginController.login(userForm);
Assert.assertEquals("OK", login);
// verify mocked call
EasyMock.verify(loginServicePartial);
EasyMock.verify(loginDao);
}
9.3. JMockit
JMockitによる部分的なモックは特に簡単です。 Expectations()\{\{}}でモック動作が定義されていないすべてのメソッド呼び出しは、「実際の」実装を使用します。
この場合、LoginService.login(UserForm)には期待が与えられていないため、実際の実装(およびLoginDAO.login(UserForm)の呼び出し)が実行されます。
@Test
public void partialMocking() {
// use partial mock
LoginService partialLoginService = new LoginService();
partialLoginService.setLoginDao(loginDao);
loginController.loginService = partialLoginService;
UserForm userForm = new UserForm();
userForm.username = "foo";
// let service's login use implementation so let's mock DAO call
new Expectations() {{
loginDao.login(userForm); result = 1;
// no expectation for loginService.login
partialLoginService.setCurrentUser("foo");
}};
String login = loginController.login(userForm);
Assert.assertEquals("OK", login);
// verify mocked call
new FullVerifications(partialLoginService) {};
new FullVerifications(loginDao) {};
}
10. 結論
この投稿では、3つのJavaモックライブラリを比較してきました。それぞれに長所と短所があります。
-
3つすべてがeasily configuredであり、モックとテスト対象オブジェクトを定義するのに役立つ注釈が付いています。ランナーは、モックインジェクションを可能な限り無痛にします。
-
Mockitoは部分的なモック用の特別な注釈があるため、ここで勝つと思いますが、JMockitはそれを必要としないので、これら2つの間の結びつきだとしましょう。
-
-
3つすべてが多かれ少なかれrecord-replay-verify patternに従いますが、私たちの意見では、ブロックでそれらを使用するように強制するJMockitが最適であるため、テストはより構造化されます。
-
Easinessの使用は重要であるため、テストを定義するための作業をできるだけ少なくすることができます。 JMockitは、常に同じ構造の選択されたオプションになります。
-
Mockitoは多かれ少なかれ最もよく知られているので、communityは大きくなります。
-
モックを使用するたびにreplayを呼び出さなければならないのは明らかなno-goなので、EasyMockにはマイナス1を付けます。
-
Consistency/simplicityも私にとって重要です。 JMockitの結果を返す方法が気に入りました。これは、「通常の」結果でも例外と同じです。
とはいえ、これまでMockitoを使用していたのは、その単純さと固定構造に魅了されていたにもかかわらず、勝者の一種としてJMockitを選択するつもりです。これから使ってみます。
このチュートリアルのfull implementationはthe GitHub projectにありますので、ダウンロードして試してみてください。