Mock
對象能夠模擬領域對象的部分行為,并且能夠檢驗運行結果是否和預期的一致。領域類將通過與
Mock
對象的交互,來獲得一個獨立的測試環境(引自《
精通
Spring——Java
輕量級架構開發實踐
》。
在模仿對象中,我們定義了四個概念:
1
)目標對象:正在測試的對象
2
)合作者對象:由目標對象創建或獲取的對象
3
)模仿對象:遵循模仿對象模式的合作者的子類(或實現)
4
)特殊化對象:覆蓋創建方法以返回模仿對象而不是合作者的目標的子類
一般來說,應用模仿對象的過程如下:
1
)創建模仿對象的實例
2
)設置模仿對象中的狀態和期望值
3
)將模仿對象作為參數來調用域代碼
4
)驗證模仿對象中的一致性
那么,我們應該如何以及在哪里使用
Mock
對象呢?一般來說,對于目標對象中的合作者對象,在測試時如果其狀態或行為的實現嚴重地依賴外部資源(比如數據持久化中的
DAO
,比如負責發送電子郵件的類),或者團隊并行開發時,目標對象的合作者對象并沒有實現(比如
J2EE
中,橫向分工時,負責
Action
的調用
Service
,負責
Service
調用
DAO
時,相應的
Service
及
DAO
沒有實現),這時我們就需要模仿這些類。其實,在做
J2EE
時,傳統的
N
層架構中,我們都是面向接口編程的,我們定義了
DAO
接口,我們定義了
Service
接口,這樣做的優點就是我們在測試時可以構造實現接口的
Mock
類。這里不得不提依賴注入,通過依賴注入,我們才能在測試時
set Mock
對象。這也說明,為了方便測試,我們不得不一步一步
重構代碼,而模式就在重構中自然地產生了。
對于
Mock
對象,我們可以根據
合作者接口(或者是類)
實現具體的
Mock
類,這樣的
Mock
類實際上是
Stub
。有些情況下,
Stub
是必要的。但對于諸如
DAO
、
Service
,我們只關心在給定參數的情況下,調用的方法能夠返回預期的值,我們根本不關心其內部實現,這時候如果使用
Stub
的話就會產生不必要的代碼。我們需要的就是能
動態地生成
Mock
對象而不需要編寫它們的工具,
EasyMock
就是這樣的工具。
EasyMock
是一個
Mock
對象的類庫,現在的版本是
2.0
,這個版本只支持
Mock
接口,如果需要
Mock
類,需要下載它的擴展包。
下面通過一個具體的例子說明一下
Mock
對象的使用,我寫的例子就是測試
Service
類中的一個方法,
Mock
的對象是
DAO
。
首先是一個簡單的實體
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();
????}
????
}
??????
????一個只含一個方法的
DAO AccountDAO:
package
?easymocktest.dao;

import
?easymocktest.domain.
*
;

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

import
?easymocktest.domain.
*
;

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