Page List

Search on the blog

2014年2月15日土曜日

スタブとモックの違い

 スタブとモックの違いがようやく分かりました。一言で言うと、
スタブとはテスト対象の間接入力をテストシナリオに合わせて都合よく書き換えるためのもの。モックとはテスト対象の間接出力が正しいことを確認するためのもの。
です[1]。

間接入力、間接出力とはその名のとおりテスト対象の間接的な入力、出力のことです。例えば、サービスクラスをテストする場合を考えてみます。

boolean createService(Entity entity)

というメソッドをテストする場合、メソッドの入力はentity、出力はtrue/falseです。
直接的な入出力はこれだけですが、サービスクラスからDaoの処理を呼び出す場合、その処理結果が間接的にサービスクラスに入力されます。同様にサービスクラスはDaoクラスを呼び出すという間接的な出力処理を行います。


実際のテストコードを使ってスタブ、モックそれぞれの使い方を考えてみます。
以下のサービスクラスをテストする場合を考えます。
package com.kenjih.sample.service;

import java.sql.SQLException;
import java.util.Date;

import com.kenjih.sample.Dao.OrderDao;
import com.kenjih.sample.component.UserContext;
import com.kenjih.sample.entity.OrderEntity;
import com.kenjih.sample.exception.BussinessException;

public class OrderServiceImpl implements OrderService {

    OrderDao orderDao;

    UserContext userContext;
    
    public boolean addOrder(OrderEntity entity) {
        if (entity == null)
            throw new BussinessException("entity is null.");

        String userId = userContext.getLoginUser();
        if (userId == null)
            throw new BussinessException("user is not logged in.");
        
        entity.setOrderedBy(userId);
        entity.setOrderedAt(new Date());        

        boolean result = false;
        try {
            result = orderDao.create(entity);
        } catch (SQLException ex) {
            throw new BussinessException("database error occurred.", ex);
        } catch (Exception ex) {
            throw new BussinessException("unknown error occurred.", ex);
        }

        return result;
    }
}
上記のテスト対象をテストする場合どのようなコードを書けばいいか考えてみます。
まず、テスト対象のすべての文が網羅されるように条件を考えます。分岐条件が引数entityに加えて
  • UserContext#getLoginUser()
  • OrderDao#create()
からの間接入力に依存しているため、間接入力を都合のいいように書き換える必要がでてきます。このときに利用するのがスタブです。

次に、テスト対象の出力が正しいことを検証します。テスト対象の戻り値(またはスローされる例外)が正しいことを確認する他に、処理の内部で行われる間接的な出力が正しいことを確認する必要があります。例えば上の処理の場合、
  • ログインユーザーが取得できなかった場合は、OrderDao#create()を呼び出さない。
  • ログインユーザーが取得できた場合は、引数Entityと同じインスタンスを使ってOrderDao#create()を一度だけ呼び出す。
などを確認する必要があります。このように間接出力が正しいことを検証するために使用するのがモックです。

以下にMockitoを利用した場合のテストコードを載せておきます。
文法上@Mockを使っていますが、実際は、orderDao、userContextともにスタブかつモックの役割を果たしています。
when(oo).thenReturn(xx)、when(oo).thenThrow(xx)の部分ではスタブとして使い、verify(oo).xxx()の部分ではモックとして使っています。

package com.kenjih.sample.service;

import static org.junit.Assert.*;
import static org.mockito.Mockito.*;

import java.sql.SQLException;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

import com.kenjih.sample.component.UserContext;
import com.kenjih.sample.entity.OrderEntity;
import com.kenjih.sample.exception.BussinessException;
import com.kenjih.sample.Dao.OrderDao;

@RunWith(MockitoJUnitRunner.class)
public class OrderServiceTest {

    @Mock
    private OrderDao orderDao;

    @Mock
    private UserContext userContext;

    @InjectMocks
    private OrderServiceImpl orderService;

    /*
     * 入力entityがnullの場合
     */
    @Test
    public void testUpdate_1() throws SQLException {
        try {
            orderService.addOrder(null);
            fail();
        } catch (BussinessException ex) {
            assertEquals("entity is null.", ex.getMessage());
            verify(userContext, never()).getLoginUser();
            verify(orderDao, never()).create((OrderEntity)anyObject());
        }
    }

    @Test
    /*
     * userContext.getLoginUser()からの間接入力がnullの場合
     */
    public void testUpdate_2() throws SQLException {
        when(userContext.getLoginUser()).thenReturn(null);

        try {
            orderService.addOrder(new OrderEntity());
            fail();
        } catch (BussinessException ex) {
            assertEquals("user is not logged in.", ex.getMessage());
            verify(userContext).getLoginUser();            
            verify(orderDao, never()).create((OrderEntity)anyObject());
        }
    }

    @Test
    /*
     * orderDao.create()からの間接入力がSQLExceptionの場合
     */
    public void testUpdate_3() throws SQLException {
        when(userContext.getLoginUser()).thenReturn("user001");
        when(orderDao.create((OrderEntity) anyObject())).thenThrow(
                new SQLException());

        try {
            orderService.addOrder(new OrderEntity());
            fail();
        } catch (BussinessException ex) {
            assertEquals("database error occurred.", ex.getMessage());
            verify(userContext).getLoginUser();            
            verify(orderDao).create((OrderEntity)anyObject());
        }
    }

    @Test
    /*
     * orderDao.create()からの間接入力がSQLException意外の例外の場合
     */
    public void testUpdate_4() throws SQLException {
        when(userContext.getLoginUser()).thenReturn("user001");
        when(orderDao.create((OrderEntity) anyObject())).thenThrow(
                new RuntimeException());

        try {
            orderService.addOrder(new OrderEntity());
            fail();
        } catch (BussinessException ex) {
            assertEquals("unknown error occurred.", ex.getMessage());
            verify(userContext).getLoginUser();            
            verify(orderDao).create((OrderEntity)anyObject());
        }
    }

    @Test
    /*
     * orderDao.create()からの間接入力がfalseの場合
     */
    public void testUpdate_5() throws SQLException {
        when(userContext.getLoginUser()).thenReturn("user001");
        when(orderDao.create((OrderEntity) anyObject())).thenReturn(false);

        OrderEntity entity = new OrderEntity();
        assertFalse(orderService.addOrder(entity));
        verify(userContext).getLoginUser();            
        verify(orderDao).create(entity);
    }

    @Test
    /*
     * orderDao.create()からの間接入力がtrueの場合
     */
    public void testUpdate_6() throws SQLException {
        when(userContext.getLoginUser()).thenReturn("user001");
        when(orderDao.create((OrderEntity) anyObject())).thenReturn(true);

        OrderEntity entity = new OrderEntity();
        assertTrue(orderService.addOrder(entity));
        verify(userContext).getLoginUser();            
        verify(orderDao).create(entity);
    }

}

References
[1] Test Double at XUnitPatterns.com

2 件のコメント:

  1. こんにちは。

    スタブとモックの考え方の視点、興味深いですね。

    返信削除
  2. 逆に分かりにくいような

    返信削除