Page List

Search on the blog

2014年2月13日木曜日

MockitoでUnitテスト効率化



 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")));    
    }
    
}

1 件のコメント:

  1. 初めまして。

    モックの使い方、勉強させていただきます!

    返信削除