接 續 EasyMock 簡介, 這邊改用另一套也為人所接受的Mock框架 JMock 來進行BookmarkDAO的 模擬,如果搭配JUnit 3來進行撰寫:
package test.cc.openhome;
import org.jmock.Expectations;
import org.jmock.Mockery;
import java.util.Arrays;
import junit.framework.TestCase;
import cc.openhome.dao.BookmarkDAO;
import cc.openhome.model.Bookmark;
import cc.openhome.model.BookmarkService;
public class BookmarkServiceTest extends TestCase {
    private Bookmark bookmark1;
    private Bookmark bookmark2;
    private BookmarkDAO mockDAO;
    private BookmarkService service;
    
    private Mockery context;
    
    public void setUp() {
        bookmark1 = new Bookmark("testURL1", "testTitle1", "testCategory1");
        bookmark2 = new Bookmark("testURL2", "testTitle2", "testCategory2");
        
        context = new Mockery();
        // 建立Mock物件
        mockDAO = context.mock(BookmarkDAO.class);
        service = new BookmarkService(mockDAO);
    }
    
    public void testAddSameBookmark() {
        // 設定預期行為
        context.checking(new Expectations() {{
            // 會呼叫DAO的get()
            oneOf(mockDAO).get();
              will(returnValue(Arrays.asList(bookmark1))); // 預期傳回值 
        }});
        
        service.add(bookmark1);
    }
    
    public void testAddDifferentBookmark() {
        context.checking(new Expectations() {{
            oneOf(mockDAO).get(); 
              will(returnValue(Arrays.asList(bookmark1)));
            oneOf(mockDAO).add(bookmark2);
        }});
        
        service.add(bookmark2);
    }
    public void tearDown() {
        // 斷言是否滿足預期行為
        context.assertIsSatisfied();
    }
}
最主要的是在設定預期行為時,會遵照以下的形式:
呼
叫次數(mock物件).方法(參數);
inSequence(sequence名稱);
when(狀態機.is(狀態));
will(動作);
then(狀態機.is(狀態));
      
      inSequence(sequence名稱);
when(狀態機.is(狀態));
will(動作);
then(狀態機.is(狀態));
呼 叫次數如oneOf、atLeast.of等,例如至少呼叫過一次mockDAO的get,則可以這麼撰寫:
atLeast(1).of(mockDAO).get();
      
      方法呼叫過後, inSequence、when、will、then都是可選擇性指定,以下例而言:
context.checking(new Expectations() {{
oneOf(mockDAO).get();
will(returnValue(Arrays.asList(bookmark1)));
}});
表示預期mockDAO會被呼叫get()一次,傳回包括bookmark1的List。在下面的程式碼中:
context.checking(new Expectations() {{
oneOf(mockDAO).get();
will(returnValue(Arrays.asList(bookmark1)));
oneOf(mockDAO).add(bookmark2);
}});
呼叫get()、add()的順序並不要緊,如果你希望呼叫的順序一定是get()或add(),則可以設定Sequence,例如:
final Sequence addDifferentBookmark =
context.sequence("addDifferentBookmark");
context.checking(new Expectations() {{
oneOf(mockDAO).get();
inSequence(addDifferentBookmark);
will(returnValue(Arrays.asList(bookmark1)));
oneOf(mockDAO).add(bookmark2);
inSequence(addDifferentBookmark);
}});
如果必須根據狀態而有對應的預期行為或切換狀態,則可以設定狀態機, 以 JMock 官網文件上的例子來說:
final States pen =
context.states("pen").startsAs("up");
...
oneOf(turtle).penDown();
then(pen.is("down")); // penDown()呼叫過後狀態為down
      
oneOf(turtle).forward(10);
when(pen.is("down")); // 只有在狀態為down時才呼叫forward()
      
oneOf(turtle).turn(90);
when(pen.is("down"));
      
oneOf(turtle).forward(10);
when(pen.is("down"));
      
oneOf(turtle).penUp();
then(pen.is("up"));
      
      ...
oneOf(turtle).penDown();
then(pen.is("down")); // penDown()呼叫過後狀態為down
oneOf(turtle).forward(10);
when(pen.is("down")); // 只有在狀態為down時才呼叫forward()
oneOf(turtle).turn(90);
when(pen.is("down"));
oneOf(turtle).forward(10);
when(pen.is("down"));
oneOf(turtle).penUp();
then(pen.is("up"));
如果搭配JUnit 4,可以如下撰寫:
package test.cc.openhome;
import org.jmock.Expectations;
import org.jmock.Mockery;
import org.jmock.Sequence;
import org.jmock.integration.junit4.JMock;
import org.jmock.integration.junit4.JUnit4Mockery;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.Arrays;
import cc.openhome.dao.BookmarkDAO;
import cc.openhome.model.Bookmark;
import cc.openhome.model.BookmarkService;
@RunWith(JMock.class)
public class BookmarkServiceTest {
    private Bookmark bookmark1;
    private Bookmark bookmark2;
    private BookmarkDAO mockDAO;
    private BookmarkService service;
    
    private Mockery context;
    
    @Before
    public void setUp() {
        bookmark1 = new Bookmark("testURL1", "testTitle1", "testCategory1");
        bookmark2 = new Bookmark("testURL2", "testTitle2", "testCategory2");
        
        context = new JUnit4Mockery();
        // 建立Mock物件
        mockDAO = context.mock(BookmarkDAO.class);
        service = new BookmarkService(mockDAO);
    }
    
    @Test
    public void testAddSameBookmark() {
        // 設定預期行為
        context.checking(new Expectations() {{
            // 會呼叫DAO的get()
            oneOf(mockDAO).get();
            // 預期傳回值
            will(returnValue(Arrays.asList(bookmark1)));
        }});
        
        service.add(bookmark1);
    }
    
    @Test
    public void testAddDifferentBookmark() {
        final Sequence addDifferentBookmark = 
            context.sequence("addDifferentBookmark");
        context.checking(new Expectations() {{
            oneOf(mockDAO).get();
              inSequence(addDifferentBookmark);
              will(returnValue(Arrays.asList(bookmark1)));
            oneOf(mockDAO).add(bookmark2);
              inSequence(addDifferentBookmark);
        }});
        
        service.add(bookmark2);
    }
}
使用JMock作 為Runner,並搭配JUnit4Mockey,會在測試方法執行過後自動驗證行為是否正確。
JMock 官方網站提供了不少的文件可作為參考。

