Mock 物件


Dummy 物件 是一種用來隔離真實環境,使受測程式不受其它物件或外在環境影響的方式。Dummy物件之所以為Dummy,就是它通常沒有複雜的行為,只單純傳回必要的 值或物件,供受測程式可以運行。

相較而言,Mock物件就複雜了一些,Mock物件的作用與 Dummy物件類似,隔離真實環境,使受測程式不受其它物件或外在環境影響,所不同的是,Mock物件模擬了真實物件的行為,真實物件被操作後應有什麼狀態變化, Mock物件就會模擬類似的變化。

舉個例子來說,你設計了以下的BookmarkService
package cc.openhome.model;

import cc.openhome.dao.BookmarkDAO;

// 其中Bookmark物件會實作equals()與hashCode()
public class BookmarkService {
private BookmarkDAO dao;
public BookmarkService(BookmarkDAO dao) {
this.dao = dao;
}
public void add(Bookmark bookmark) {
if(!dao.get().contains(bookmark)) {
dao.add(bookmark);
}
}
}

BookmarkService 委託BookmarkDAO來作資料庫增刪查找的動作:
package cc.openhome.dao;

import cc.openhome.model.Bookmark;
import java.util.List;

public interface BookmarkDAO {
public List<Bookmark> get();
public void add(Bookmark bookmark);
public void delete(Bookmark bookmark);
}

檢查目前是否有重複書籤記錄的商務邏輯由BookmarkService負責,假設這邊的作法是從BookmarkDAO取得所有書籤並比較,若無重複再新增書籤 至資料庫。顯然這邊要能測試BookmarkService,就一定得有個BookmarkDAO實作,但你的目的僅在測試 BookmarkService的add(),而不是要連同BookmarkDAO實作物件一同測試。

因此在這邊你要有個假貨,這個假貨會有BookmarkDAO的增刪查找行為,但又不是真正與資料庫作
增刪查找,因此你可以這麼設計:
package test.cc.openhome;

import static org.junit.Assert.*;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import cc.openhome.dao.BookmarkDAO;
import cc.openhome.model.Bookmark;
import cc.openhome.model.BookmarkService;

class MockDAO implements BookmarkDAO {
private List<Bookmark> bookmarks = new ArrayList<Bookmark>();
public void add(Bookmark bookmark) {
bookmarks.add(bookmark);
}

public void delete(Bookmark bookmark) {
bookmarks.remove(bookmark);
}

public List<Bookmark> get() {
return bookmarks;
}
}

public class BookmarkServiceTest {
@Test
public void testAdd() {
BookmarkDAO mockDAO = new MockDAO();
Bookmark bookmark1 =
new Bookmark("testURL1", "testTitle1", "testCategory1");
Bookmark bookmark2 =
new Bookmark("testURL2", "testTitle2", "testCategory2");
mockDAO.add(new Bookmark("testURL1", "testTitle1", "testCategory1"));

BookmarkService service = new BookmarkService(mockDAO);
service.add(bookmark1);
assertEquals(1, mockDAO.get().size());

service.add(bookmark2);
assertEquals(2, mockDAO.get().size());
assertEquals(bookmark2, mockDAO.get().get(1));
}
}

實際上需要
Mock物件進行測試時,有一些現成框架可以使 用,像是EasyMock或JMock,這之後會再說明。

再觀察
Dummy 物件 中的例子,其實你要的傳回一個InputStream, 原本該文中範例的作法最後的作法是,實作一個DummyURLConnection作為Dummy物件,傳回測試所需的InputStream,其實那個傳回的 InputStream,就是個Mock物件的概念,因為它並不是真正代表 HTTP連結的輸入串流,只不過用來與HttpHelper的getContent()作互動,你真正想測試的,其實是取回的InputStream與 getContent()中其它部份,實作是否正確。

你可以修改一下HttpHelper:
package cc.openhome;

import java.io.*;
import java.net.URL;

public class HttpHelper {
public String getContent(URL url) throws IOException {
InputStream input = createInputStream(url);
StringWriter writer = new StringWriter();
byte[] data = new byte[2048];
int length = -1;
while((length = input.read(data)) != -1) {
writer.write(new String(data, 0, length));
}
input.close();
writer.close();
return writer.toString();
}

protected InputStream createInputStream(URL url) throws IOException {
return url.openStream();
}
}

為了要能進行測 試,傳回InputStream的假貨,你的測試程式可以如下撰寫:
package test.cc.openhome;

import static org.junit.Assert.assertEquals;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import org.junit.Test;
import cc.openhome.HttpHelper;

class TestForHttpHelper extends HttpHelper {
@Override
protected InputStream createInputStream(URL url) throws IOException {
// 傳回的 InputStream 就是 Mock 物件的概念
return new ByteArrayInputStream(new String("success").getBytes());
}
}

public class HttpHelperTest {
@Test
public void testGetContent() throws Exception {
HttpHelper helper = new TestForHttpHelper();
String expected = "success";
String result = helper.getContent(new URL("http://localhost:8080"));
assertEquals(expected, result);
}
}