Mock 對(duì)象能夠模擬領(lǐng)域?qū)ο蟮牟糠中袨椋⑶夷軌驒z驗(yàn)運(yùn)行結(jié)果是否和預(yù)期的一致。領(lǐng)域類將通過與 Mock 對(duì)象的交互,來獲得一個(gè)獨(dú)立的測(cè)試環(huán)境(引自《 精通 Spring——Java 輕量級(jí)架構(gòu)開發(fā)實(shí)踐 》。
在模仿對(duì)象中,我們定義了四個(gè)概念:
1 )目標(biāo)對(duì)象:正在測(cè)試的對(duì)象
2 )合作者對(duì)象:由目標(biāo)對(duì)象創(chuàng)建或獲取的對(duì)象
3 )模仿對(duì)象:遵循模仿對(duì)象模式的合作者的子類(或?qū)崿F(xiàn))
4 )特殊化對(duì)象:覆蓋創(chuàng)建方法以返回模仿對(duì)象而不是合作者的目標(biāo)的子類
一般來說,應(yīng)用模仿對(duì)象的過程如下:
1 )創(chuàng)建模仿對(duì)象的實(shí)例
2 )設(shè)置模仿對(duì)象中的狀態(tài)和期望值
3 )將模仿對(duì)象作為參數(shù)來調(diào)用域代碼
4 )驗(yàn)證模仿對(duì)象中的一致性
那么,我們應(yīng)該如何以及在哪里使用 Mock 對(duì)象呢?一般來說,對(duì)于目標(biāo)對(duì)象中的合作者對(duì)象,在測(cè)試時(shí)如果其狀態(tài)或行為的實(shí)現(xiàn)嚴(yán)重地依賴外部資源(比如數(shù)據(jù)持久化中的 DAO ,比如負(fù)責(zé)發(fā)送電子郵件的類),或者團(tuán)隊(duì)并行開發(fā)時(shí),目標(biāo)對(duì)象的合作者對(duì)象并沒有實(shí)現(xiàn)(比如 J2EE 中,橫向分工時(shí),負(fù)責(zé) Action 的調(diào)用 Service ,負(fù)責(zé) Service 調(diào)用 DAO 時(shí),相應(yīng)的 Service 及 DAO 沒有實(shí)現(xiàn)),這時(shí)我們就需要模仿這些類。其實(shí),在做 J2EE 時(shí),傳統(tǒng)的 N 層架構(gòu)中,我們都是面向接口編程的,我們定義了 DAO 接口,我們定義了 Service 接口,這樣做的優(yōu)點(diǎn)就是我們?cè)跍y(cè)試時(shí)可以構(gòu)造實(shí)現(xiàn)接口的 Mock 類。這里不得不提依賴注入,通過依賴注入,我們才能在測(cè)試時(shí) set Mock 對(duì)象。這也說明,為了方便測(cè)試,我們不得不一步一步 重構(gòu)代碼,而模式就在重構(gòu)中自然地產(chǎn)生了。
對(duì)于 Mock 對(duì)象,我們可以根據(jù) 合作者接口(或者是類) 實(shí)現(xiàn)具體的 Mock 類,這樣的 Mock 類實(shí)際上是 Stub 。有些情況下, Stub 是必要的。但對(duì)于諸如 DAO 、 Service ,我們只關(guān)心在給定參數(shù)的情況下,調(diào)用的方法能夠返回預(yù)期的值,我們根本不關(guān)心其內(nèi)部實(shí)現(xiàn),這時(shí)候如果使用 Stub 的話就會(huì)產(chǎn)生不必要的代碼。我們需要的就是能 動(dòng)態(tài)地生成 Mock 對(duì)象而不需要編寫它們的工具, EasyMock 就是這樣的工具。
EasyMock 是一個(gè) Mock 對(duì)象的類庫,現(xiàn)在的版本是 2.0 ,這個(gè)版本只支持 Mock 接口,如果需要 Mock 類,需要下載它的擴(kuò)展包。
下面通過一個(gè)具體的例子說明一下 Mock 對(duì)象的使用,我寫的例子就是測(cè)試 Service 類中的一個(gè)方法, Mock 的對(duì)象是 DAO 。
首先是一個(gè)簡單的實(shí)體 bean Account:
package easymocktest.domain;

import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;


public class Account {

private Long id;
private String name;
private String pwd;

public Long getId() {
return id;
}
public void setId(Long id) {
this .id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this .name = name;
}
public String getPwd() {
return pwd;
}
public void setPwd(String pwd) {
this .pwd = pwd;
}
/**
* @see java.lang.Object#toString()
*/
public String toString() {
return new ToStringBuilder( this , ToStringStyle.MULTI_LINE_STYLE)
.append( " name " , this .name).append( " pwd " , this .pwd).append( " id " ,
this .id).toString();
}
}
一個(gè)只含一個(gè)方法的 DAO AccountDAO:
package easymocktest.dao;

import easymocktest.domain. * ;

public interface AccountDAO {
public Account getByNameAndPwd(String name,String pwd);
}
一個(gè)同樣只含一個(gè)方法的 AccountService 接口:
package easymocktest.service;

import easymocktest.domain. * ;

public interface AccountService {
public Account getAccount(String name,String pwd);
}
與 AccountService 相應(yīng)的實(shí)現(xiàn)類:
package easymocktest.service.impl;

import easymocktest.service.AccountService;
import easymocktest.dao. * ;
import easymocktest.domain.Account;

public class AccountServiceImpl implements AccountService {

private AccountDAO accountDAO;


public AccountDAO getAccountDAO() {
return accountDAO;
}
public void setAccountDAO(AccountDAO accountDAO) {
this .accountDAO = accountDAO;
}
public Account getAccount(String name, String pwd) {
return this .accountDAO.getByNameAndPwd(name, pwd);
}
}
這里我沒有實(shí)現(xiàn) AccountDAO 接口,對(duì)于 Mock 測(cè)試來說,這也是不需要的。下面就是 AccountServiceImpl 的測(cè)試類 AccountServiceTest :
package easymocktest.service;

import junit.framework. * ;
import easymocktest.dao. * ;
import easymocktest.domain. * ;
import easymocktest.service.impl. * ;
import static org.easymock.EasyMock.createMock;
import static org.easymock.EasyMock.replay;
import static org.easymock.EasyMock.reset;
import static org.easymock.EasyMock.verify;
import static org.easymock.EasyMock.expect;

public class AccountServiceTest extends TestCase {

private AccountDAO accountDAOMock;
private AccountServiceImpl accountService;
@Override

protected void setUp() throws Exception {
accountDAOMock = createMock(AccountDAO. class );
accountService = new AccountServiceImpl();
accountService.setAccountDAO(accountDAOMock);
}

public void testGetAccount() {
String name = " kafka " ;
String pwd = " 0102 " ;
Account a = new Account();
a.setName(name);
a.setPwd(pwd);
a.setId( new Long( 10 ));
reset(accountDAOMock); // (a)
expect(accountDAOMock.getByNameAndPwd(name, pwd)).andReturn(a); // (b)
replay(accountDAOMock); // (c)
Account b = accountService.getAccount(name, pwd);
assertEquals(a, b);
verify(accountDAOMock); // (d)
}
}
下面簡要的說明一下 Mock 對(duì)象的工作過程:
1 )在 setUp() 中,通過 “accountDAOMock = createMock(AccountDAO.class);” (這里使用了 java5 中的靜態(tài)導(dǎo)入),創(chuàng)建 AccountDAO 的 Mock 對(duì)象,由于 EasyMock 采用了范型技術(shù),故創(chuàng)建的 Mock 對(duì)象不需要強(qiáng)制類型轉(zhuǎn)換。然后通過 “accountService.setAccountDAO(accountDAOMock);” 設(shè)置目標(biāo)對(duì)象的合作者對(duì)象。
2 )對(duì)于測(cè)試方法 “testGetAccount()” , (a) 處的 reset() 方法是將 Mock 對(duì)象復(fù)位,也就是重新設(shè)置 Mock 對(duì)象的狀態(tài)和行為。由于此處是第一次調(diào)用 Mock 對(duì)象,可以不必使用 reset() 方法。
3 ) (b) 處 expect() 是錄制 Mock 對(duì)象方法的調(diào)用,其參數(shù)就是 Mock 對(duì)象的方法,其中如果調(diào)用的方法有返回值,要通過 andReturn() 方法設(shè)置預(yù)期的返回值。
4 ) (c) 處的 replay() 是結(jié)束錄制過程。 在調(diào)用 replay() 方法之前的狀態(tài), EashMock 稱之為 “record 狀態(tài) ” 。該狀態(tài)下, Mock 對(duì)象不具備行為(即模擬接口的實(shí)現(xiàn)),它僅僅記錄方法的調(diào)用。在調(diào)用 replay() 后,它才以 Mock 對(duì)象預(yù)期的行為進(jìn)行工作,檢查預(yù)期的方法調(diào)用是否真的完成。
5 ) (d) 處的 verify() 是用于在錄制和回放兩個(gè)步驟完成之后進(jìn)行預(yù)期和實(shí)際結(jié)果的檢查。這里就是檢查 accountDAOMock 是否如預(yù)期一樣調(diào)用了 getByNameAndPwd 方法。
對(duì)于上面的舉例,它可能并不具有實(shí)際的價(jià)值,這里我只想拋磚引玉。在 N 層架構(gòu)的 Java 程序中, Mock 對(duì)象在單元測(cè)試中正發(fā)揮著越來越重要的作用。我現(xiàn)在看到的是,在 Service 層與 Web 層, Mock 對(duì)象能很好的被應(yīng)用。有人覺得在 Persistence 層也應(yīng)該使用 Mock 對(duì)象,但就像我們所知道的,在使用 Hibernate 、 Ibatis 等 ORM 工具的情況下,我們的 Persistence 層的測(cè)試主要測(cè)試的就是那些配置文件、查詢語句等(實(shí)際上是集成測(cè)試),如果還 Mock 的話,就失去了測(cè)試的意義。
對(duì)于 Mock 的更多的信息,你可以訪問Mock Objects,在本文寫作的過程中,參考了使用模仿對(duì)象進(jìn)行單元測(cè)試
等文章,一并感謝。