JMock 簡介


接 續 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(狀態));

呼 叫次數如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"));

如果搭配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 官方網站提供了不少的文件可作為參考。