使用JMock來實現孤立測試(轉)
?
我們在測試某類時,由于它要與其他類發生聯系,因此往往在測試此類的代碼中也將與之聯系的類也一起測試了。這種測試,將使被測試的類直接依賴于其他
類,一旦其他類發生改變,被測試類也隨之被迫改變。更重要的是,這些其他類可能尚未經過測試,因此必須先測試這些類,才能測試被測試類。這種情況下,測試
驅動開發成為空談。而如果其他類中也引用了被測試類,我們到底先測試哪一個類?因此,在測試中,如果我們能將被測試類孤立起來,使其完全不依賴于其他類的
具體實現,這樣,我們就能做到測試先行,先測試哪個類,就先實現哪個類,而不管與之聯系的類是否已經實現。
虛擬對象(mock
object)就是為此需要而誕生的。它通過JDK中的反射機制,在運行時動態地創建虛擬對象。在測試代碼中,我們可以驗證這些虛擬對象是否被正確地調用
了,也可以在明確的情況下,讓其返回特定的假想值。而一旦有了這些虛擬對象提供的服務,被測試類就可以將虛擬對象作為其他與之聯系的真實對象的替身,從而
輕松地搭建起一個很完美的測試環境。
JMock是幫助創建mock對象的工具,它基于Java開發,在Java測試與開發環境中有不可比擬的優勢,更重要的是,它大大簡化了虛擬對象的使用。
本文中,通過一個簡單的測試用例來說明JMock如何幫助我們實現這種孤立測試。有三個主要的類,User,UserDAO,及
UserService。本文中,我們只需測試UserService,準備虛擬UserDAO。對于User,由于本身僅是一個過于簡單的POJO,可
以不用測試。但如果你是一個完美主義者,也可以使用JMock的虛擬它。在這領域,JMock幾乎無所不能。:)
User是一個POJO,用以在視圖中傳輸數據及映射數據庫。其代碼如下:
package com.sarkuya.model;
public class User {
??? private String name;
??? public User() {
??? }
???
??? public User(String name) {
??????? this.name = name;
??? }
???
??? public String getName() {
??????? return name;
??? }
???
??? public void setName(String name) {
??????? this.name = name;
??? }
}
UserDAO負責與數據庫打交道,通過數據庫保存、獲取User的信息。盡管我們可以不用知道JMock如何通過JDK的反射機制來實現孤立測試,但至
少應知道,JDK的反射機制要求這些在運行時創建的動態類必須定義接口。在使用JMock的環境中,由于我們要虛擬UserDAO,意味著UserDAO
必須定義接口。代碼如下:
package com.sarkuya.dao;
import com.sarkuya.model.User;
public interface UserDAO {
??? public void saveUser(User user);
??? public User getUser(Long id);
}
UserService存有UserDAO的引用,通過其對外提供應用級的服務。相應地,我們先定義了其接口(盡管在本文中,作為被測試類,UserService不需要有接口,但如果以后此類需要被虛擬,也應該帶有接口,基于此原因,我們也為其定義了接口)。
package com.sarkuya.service;
import com.sarkuya.dao.UserDAO;
import com.sarkuya.model.User;
public interface UserService {
??? public void setUserDAO(UserDAO userDAO);
???
??? public void saveUser(User user);
??? public User getUser(Long id);
}
可以看到,除了setUserDAO()外,其另外的方法與UserDAO一樣。這是設計模式中門面模式的典型應用,應用只通過UserService提供服務,而UserService在內部通過調用UserDAO來實現相應的功能。
根據測試先行的原則,你應該先寫測試,再編寫實現。這里先編寫實現的原因,主要是使讀者更加清楚我們接著要測試什么。由于本文是著重介紹JMock的使用,加上UserServiceImpl比較簡單,因此先列出其代碼如下:
package com.sarkuya.service.impl;
import com.sarkuya.dao.UserDAO;
import com.sarkuya.model.User;
import com.sarkuya.service.UserService;
public class UserServiceImpl implements UserService {
??? private UserDAO userDAO;
???
??? public UserServiceImpl() {
??? }
??? public void setUserDAO(UserDAO userDAO) {
??????? this.userDAO = userDAO;
??? }
??? public User getUser(Long id) {
??????? return userDAO.getUser(id);
??? }
??? public void saveUser(User user) {
??????? userDAO.saveUser(user);
??? }
}
下面是UserService的測試代碼:
package com.sarkuya.service;
import com.sarkuya.dao.UserDAO;
import com.sarkuya.model.User;
import com.sarkuya.service.impl.UserServiceImpl;
import junit.framework.*;
import org.jmock.Mock;
import org.jmock.MockObjectTestCase;
public class UserServiceTest extends MockObjectTestCase {
??? private UserService userService = new UserServiceImpl();
???
??? private Mock userDAO = null;
???
??? public UserServiceTest(String testName) {
??????? super(testName);
??? }
???
??? protected void setUp() throws Exception {
??????? userDAO = new Mock(UserDAO.class);
??????? userService.setUserDAO((UserDAO)userDAO.proxy());
??? }
???
??? protected void tearDown() throws Exception {
??? }
???
??? public static Test suite() {
??????? TestSuite suite = new TestSuite(UserServiceTest.class);
???????
??????? return suite;
??? }
???
??? public void testGetUser() {
??????? User fakeUser = new User("John");
??????? userDAO.expects(once()).method("getUser").with(eq(1L)).will(returnValue(fakeUser));
???????
??????? User user = userService.getUser(1L);
??????? assertNotNull(user);
??????? assertEquals("John", user.getName());
??? }
???
??? public void testSaveUser() {
??????? User fakeUser = new User("John");
???????
??????? userDAO.expects(once()).method("getUser").with(eq(1L)).will(returnValue(fakeUser));
??????? User user = userService.getUser(1L);
??????? assertEquals("John", user.getName());
???????
??????? userDAO.expects(once()).method("saveUser").with(same(fakeUser));
??????? user.setName("Mike");
??????? userService.saveUser(user);
???????
??????? userDAO.expects(once()).method("getUser").with(eq(1L)).will(returnValue(user));
??????? User modifiedUser = userService.getUser(1L);
??????? assertEquals("Mike", user.getName());
??? }
}
此段代碼有幾點應注意:
1、此測試類繼承了JMock的MockObjectTestCase
2、private Mock userDAO = null;說明userDao是一個準備虛擬的對象
3、在setup()中,將userDAO.class傳入Mock()后,再通過proxy()方法返回一個UserDAO的代理類實例(即虛擬對象實例),并賦值于userService
4、在testGetUser()方法中,如果我們先將第一行及第二行代碼屏蔽掉,可以看出,這是一個真實環境下的測試代碼。先獲取一個User,
然后確認其非空值,再確認其姓名為“John”。此時,在真實環境下,這段代碼要測試成功的前提必須是UserDAO已經連接到了數據庫,然后返回一個
User后傳給UserService。
但問題是,到目前為止,且不說UserDAO還未經歷連接數據庫這一系列繁瑣而痛苦的過程,我們甚至還未實現UserDAO的接口!那么,為何加上第一行
及第二行代碼后就可以了呢?這正是JMock的威力所在。先實例化一個測試用的fakeUser,然后通過一系列的指令,在第二行代碼中告訴JMock應
該如何“做假”。盡管這句代碼很長,我們可作如下理解:
1) userDAO.expects(once()):我們期望userDAO的某方法被執行一次,如果此方法未被執行,或者執行了二次以上,測試就不會通過
2) method("getUser"):這個期望被執行一次的方法名為userDAO.getUser()
3) with(eq(1L)):執行getUser()方法時,確認其傳入的參數值為“1L”
4) will(returnValue(fakeUser)):上述條件均滿足后,返回一個虛假的對象,即我們前面實例化的fakeUser
總體來說,當設定好第二行語句后,JMock就在后臺監控著,確保userDAO.getUser()必須,且只被執行一次,且參數“1L”已經正確地傳給了此方法,一旦這些條件被滿足,就返回fakeUser。
而在第三行,User user = userService.getUser(1L)將觸發所有這些條件,作為獎勵,它接受了獎品fakeUser并賦值于user對象。而下面第四行及第五行均對此user對象進行測試,不通過才怪。
5) testSaveUser()方法中的原理類似。其思路是,將id為“1”的user從數據庫中取出,將其名改為“Mike”,再存回數據庫,然后再從數據庫中取出此user,確保其名字已被改變。
第五行userDAO.expects(once()).method("saveUser").with(same(fakeUser))比較特殊。首
先,with(same(fakeUser))說明,傳入參數必須是fakeUser此實例,盡管我們在下面的語句中通過user.setName
("Mike"),但只是改變了其name的屬性,而fakeUser的實例引用并未發生改變,因此可以滿足條件。其次,其后沒有.will
(returnValue(fakeUser)),因為userDAO.saveUser()不需要返回任何對象或基本數據類型。
另外,當再次執行userDAO.expects()時,JMock將重設其監控條件。我們也可以通過userDAO.reset()來顯式是清除監控條件。
通過以上實例代碼及其說明,我們看出,用好JMock的關鍵是先設置監控條件,再寫相應的測試語句。一旦設好監控條件后,在某段代碼塊執行完畢時,
如果監控條件未得到滿足,或是沒有通過expects()再次重設條件,或通過reset()來顯式是清除監控條件,測試將無法通過。
以上介紹了JMock的基本使用方法。而這種基本用法,占了全面掌握JMock所需學習的知識70%以上。關于JMock的更多細節,感興趣的讀者可以訪問JMock的網站進一步學習。
posted on 2007-04-10 14:40
想飛的魚 閱讀(591)
評論(0) 編輯 收藏 所屬分類:
java