Mockitoというテストフレームワークがとても便利です。いわゆるモッキングフレームワークなのですが、SpringなどのDIコンテナを使って依存性を注入しているようなケースでもスタブ/モックを簡単に作ってくれて使いやすいです。
準備
Mavenを使って必要なライブラリをインストールします。pom.xmlに以下を記述してください。
直接関係はありませんが、サンプルでCommons Langも使っているためそれもdependenciesに含めています。
<dependencies>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>1.9.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.0</version>
</dependency>
</dependencies>
簡単な機能の使用例
Mockitoの機能を使ってみます。djUnitの
Virtual Mock Objectsのようなことが出来ます。
package com.kenjih.mockito;
import java.util.LinkedList;
import java.util.List;
import org.junit.Test;
import static org.mockito.Mockito.*;
import static org.junit.Assert.*;
public class Sample {
@Test
public void runTest1() {
List mockedList = mock(List.class);
mockedList.add("hoge");
mockedList.clear();
mockedList.add("fuga");
mockedList.add("fuga");
mockedList.add("fuga");
// "hoge"を引数にしてaddメソッドは一度だけ呼ばれたか?
verify(mockedList).add("hoge");
// clearメソッドは一度だけ呼ばれたか?
verify(mockedList).clear();
// "fuga"を引数にしてaddメソッドはちょうど3回呼ばれたか?
verify(mockedList, times(3)).add("fuga");
// isEmptyメソッドは呼ばれていないか?
verify(mockedList, never()).isEmpty();
}
@Test
public void runTest2() {
LinkedList mockedList = mock(LinkedList.class);
// mockedList.get(0)が呼ばれたら必ず"first"を返すように設定。
when(mockedList.get(0)).thenReturn("first");
// mockedList.get(1)が呼ばれたらRuntimeExceptionをスローするように設定。
when(mockedList.get(1)).thenThrow(new RuntimeException());
assertEquals("first", mockedList.get(0));
assertEquals("first", mockedList.get(0));
try {
mockedList.get(1);
fail();
} catch(RuntimeException e) {
}
// 値が設定されていない場合はnullを返す。
assertNull(mockedList.get(2));
// 任意の引数に対して、mockedList.getは"hoge"を返すように設定。
when(mockedList.get(anyInt())).thenReturn("hoge");
for (int i = 0; i < 10; i++)
assertEquals("hoge", mockedList.get(i));
}
}
Mockitoを用いたUnit Testの例
簡単な使い方を紹介したところで、もう少し実用的な例を考えます。 例としてサービスクラスの単体テストを考えます。サービスクラスはDAOを持っていて、DAOの部分をモック化してテストを行います。以下の例では、DAOはコンストラクタ/セッターでセットされる想定になっていますが、SpringなどのDIコンテナからDAOをインジェクションしている場合にも同様の方法でモックを使うことが出来ます。
まず、テスト対象のサービスクラスです。updateメソッドをテストすることを考えてみてください。(サンプルなので処理はかなり適当に書いてます)
package com.kenjih.mockito;
import java.sql.SQLException;
public class Service {
private Dao dao;
public boolean update(Entity entity) {
if (entity == null) {
return false;
}
boolean ret = false;
try {
ret = dao.update(entity);
} catch (SQLException e) {
throw new BussinessException("Database access error occurred.", e);
} catch (RuntimeException e) {
throw new BussinessException("Unknown error occurred.");
}
if (!ret) {
throw new BussinessException("data was not updated.");
}
return true;
}
}
サービスクラスから参照されているクラスの定義は以下のとおりです。
package com.kenjih.mockito;
import java.sql.SQLException;
public interface Dao {
boolean create(Entity entity) throws SQLException;
Entity read(int id) throws SQLException;
boolean update(Entity entity) throws SQLException;
boolean delete(Entity entity) throws SQLException;
}
package com.kenjih.mockito;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
public class Entity {
private String name;
public Entity() {}
public Entity(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public int hashCode() {
return new HashCodeBuilder().append(name).toHashCode();
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Entity)) {
return false;
}
if (this == obj) {
return true;
}
Entity rhs = (Entity) obj;
return new EqualsBuilder().append(name, rhs.name).isEquals();
}
}
package com.kenjih.mockito;
public class BussinessException extends RuntimeException {
private static final long serialVersionUID = 2986073923681825950L;
public BussinessException() {
super();
}
public BussinessException(String message) {
super(message);
}
public BussinessException(String message, Throwable throwable) {
super(message, throwable);
}
public BussinessException(Throwable throwable) {
super(throwable);
}
}
そして以下がテスト実行クラスです。@MockでmockedDaoがモックになっているところと、@InjectMocksでserviceにモック化されたオブジェクトが注入されているところがポイントです。
また、@Beforeが付与されたメソッドでモックの挙動を定義しています。mockedDao#update()メソッドを呼んだ場合の戻り値を、引数ごとに指定しています(どの引数パターンにマッチするかの判定には引数Entityのequalsメソッドが使われます)。
package com.kenjih.mockito;
import static org.mockito.Mockito.*;
import static org.junit.Assert.*;
import java.sql.SQLException;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class TestService {
@Mock
private Dao mockedDao;
@InjectMocks
private Service service; // Mockオブジェクト注入
@Rule
public ExpectedException exception = ExpectedException.none();
@Before
public void init() throws SQLException {
// mockedDaoの振る舞いを設定する。
when(mockedDao.update(new Entity("DB_ERR"))).thenThrow(new SQLException());
when(mockedDao.update(new Entity("UNKNOWN_ERR"))).thenThrow(new RuntimeException());
when(mockedDao.update(new Entity("FALSE"))).thenReturn(false);
when(mockedDao.update(new Entity("SUCCESS"))).thenReturn(true);
}
@Test
public void testUpdate_1() {
assertEquals(false, service.update(null));
}
@Test
public void testUpdate_2() {
exception.expect(BussinessException.class);
exception.expectMessage("Database access error occurred.");
service.update(new Entity("DB_ERR"));
}
@Test
public void testUpdate_3() {
exception.expect(BussinessException.class);
exception.expectMessage("Unknown error occurred.");
service.update(new Entity("UNKNOWN_ERR"));
}
@Test
public void testUpdate_4() {
assertTrue(service.update(new Entity("SUCCESS")));
}
}