使用模擬物件(Mock Object)技術進行測試驅動開發
敏捷開發
敏捷軟體開發又稱敏捷開發,是一種從上世紀 90 年代開始引起開發人員注意的新型軟體開發方法。和傳統瀑布式開發方法對比,敏捷開發強調的是在幾周或者幾個月很短的時間週期,完成相對較小功能,並交付使用。在專案週期內不斷改善和增強。
2001 年初,在美國猶他州雪鳥滑雪勝地,17 名程式設計大師分別代表極限程式設計、Scrum、特徵驅動開發、動態系統開發方法、自適應軟體開發、水晶方法、實用程式設計等開發流派,發表“敏捷軟體開發”宣言。其內容主要包括:
- 人和互動重於過程和工具;
- 可以工作的軟體重於求全責備的文件;
- 客戶協作重於合同談判;
- 隨時應對變化重於循規蹈矩;
可見在敏捷軟體開發中,交付高質量的軟體是非常重要的。只有交付可以工作的軟體,開發人員才能不斷地完成更多功能,而不是將大部分時間投入在修復軟體產品缺陷 (Bug) 。所以如何提高交付軟體的質量,在敏捷開發實施過程非常重要。
測試驅動開發
測試驅動開發,它是敏捷開發的最重要的部分。方法主要是先根據客戶的需求編寫測試程式,然後再編碼使其通過測試。在敏捷開發實施中,開發人員主要從兩個方面去理解測試驅動開發。
- 在測試的輔助下,快速實現客戶需求的功能。通過編寫測試用例,對客戶需求的功能進行分解,並進行系統設計。我們發現從使用角度對程式碼的設計通常更符合後期開發的需求。可測試的要求,對程式碼的內聚性的提高和複用都非常有益。
- 在測試的保護下,不斷重構程式碼,提高程式碼的重用性,從而提高軟體產品的質量。可見測試驅動開發實施的好壞確實極大的影響軟體產品的質量,貫穿了軟體開發的始終。
在測試驅動開發中,為了保證測試的穩定性,被測程式碼介面的穩定性是非常重要的。否則,變化的成本就會急劇的上升。所以,自動化測試將會要求您的設計依賴於介面,而不是具體的類。進而推動設計人員重視介面的設計,體現系統的可擴充套件性和抗變性。
利用偽物件 (Mock Obect) 實現介面測試
在實施測試驅動開發過程中,我們可能會發現需要和系統內的某個模組或系統外某個實體互動,而這些模組或實體在您做單元測試的時候可能並不存在,比如您遇到了資料庫,遇到了驅動程式等。這時開發人員就需要使用 MO 技術來完成單元測試。
最開始,Mock Object 是完全由測試者自己手工撰寫的。在這裡我們可以舉個簡單的例子。
我們有一個移動數字電視卡的介面程式。
清單 1. VideoCardInterface 程式碼
1 2 3 4 5 6 7 8 9 10 11 12 |
public interface VideoCardInterface {
public void open();
public void changeChannel(int i);
public void close();
public Byte[] read();
public boolean fault();
}
|
下面是每個方法的功能說明:
- open 開啟移動數字電視卡。
- changeChannel 切換移動數字電視訊道。必須在開啟之後才可以正常工作,否則就提示錯誤資訊。
- close 關閉移動移動電視卡。必須在開啟之後才可以正常工作,否則就提示錯誤資訊。
- read 讀取位元組流。必須在開啟之後才可以正常工作,否則就提示錯誤資訊。
- fault 顯示當前工作狀態。
由於相對應的硬體開發工作還沒有完成,我們無法基於這樣的介面程式進行實際的測試。所以開發人員基於介面,實現了部分移動電視卡的邏輯。
清單 2. MockVCHandler 程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
public class MockVCHandler implements VideoCardInterface {
private boolean initialized = false;
private boolean error = false;
private int channel;
private static final int DEFAULTCHANNEL = 1;
public void open() {
initialized = true;
channel = DEFAULTCHANNEL;
}
public void changeChannel(int i) {
if (!initialized) {
Assert.fail("Trying to change channel before open");
}
if (i <= 0) {
Assert.fail("Specified channale is out-of-range");
}
this.channel = i;
}
public void close() {
if (!initialized) {
Assert.fail("Trying to close before open");
}
}
public Byte[] read() {
if (!initialized) {
Assert.fail("Trying to read before open");
return null;
}
if (channel > 256) {
error = true;
Assert.fail("Channel is out-of-range");
}
return new Byte[] { '0', '1' };
}
public boolean fault() {
return error;
}
}
|
通過以上的實現,我們可以測試每個動作之間的先後邏輯關係,同時可以測試資料流讀取出錯的邏輯。如果測試人員進一步對資料流進行測試。還可以自我生成一段二進位制位元組流並進行測試。通過以上的實現,我們可以大大加快開發進度。在傳統開發流程中,軟體開發和測試不得不等大部分硬體都已經可以使用的條件下才可以開發測試。使用 MO 技術,使得硬體和軟體在一定程度上可以同步開發和測試。
但是測試人員完全依靠自己實現這些類,不可避免的會帶來測試用例編寫效率低下和測試用例編寫困難的弊病,甚至可能會影響 XP 實踐者“測試先行”的激情。此時,各種各樣幫助建立 Mock Object 的工具就應運而生了。目前,在 Java 陣營中主要的 Mock 測試工具有 jMock,MockCreator,MockRunner,EasyMock,MockMaker 等,在微軟的 .Net 陣營中主要是 NMock,.NetMock,Rhino Mocks 和 Moq 等。
jMock 框架介紹
總體上來說,jMock 是一個輕量級的模擬物件技術的實現。它具有以下特點:
- 可以用簡單易行的方法定義模擬物件,無需破壞本來的程式碼結構表;
- 可以定義物件之間的互動,從而增強測試的穩定性;
- 可以整合到測試框架;
- 易擴充;
與大多數 MOCK 框架一樣,我們可以在 IDE 中使用並進行開發。本文以最常用的 Eclipse 為例。
下載 jMock
在 jMock 官方網站,我們可以下載當前穩定版本 jMock2.5.1 。
配置類路徑
為了使用 jMock 2.5.1,您需要加入下面的 JAR 檔案到當前的類路徑。
- jmock-2.5.1.jar
- hamcrest-core-1.1.jar
- hamcrest-library-1.1.jar
圖 1. 已新增到 TestingExample 專案中 jMock 的 JAR 檔案
使用 jMock 模擬介面
我們首先必須引入 jMock 的類,定義我們的測試類,建立一個 Mockery 的物件用來代表上下文。上下文可以模擬出物件和物件的輸出,並且還可以檢測應用是否合法。
1 2 3 4 5 6 7 8 |
import org.jmock.Mockery;
import org.jmock.Expectations;
public class AJmockTestCase {
Mockery context = new Mockery();
}
|
然後我們建立一個 calcService 去模擬 ICalculatorService 介面。在這裡我們以 add() 方法為例,我們針對 add() 方法定義預期值 assumedResult 。之後我們去呼叫 add(1,1) 時,就可以得到預期值。
1 2 3 4 5 6 7 8 9 |
// set up
final ICalculatorService calcService = context.mock(ICalculatorService.class);
final int assumedResult = 2;
// expectations
context.checking(new Expectations() {{
oneOf (calcService).add(1, 1); will(returnValue(assumedResult));
}});
|
清單 3 和 4 分別顯示了 ICalculatorService 和 AJmockTestCase 的程式碼。
清單 3. ICalculatorService 程式碼
1 2 3 4 5 |
public interface ICalculatorService {
public int add(int a, int b);
}
|
清單 4. AJmockTestCase 程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
import org.jmock.Mockery;
import org.jmock.Expectations;
public class AJmockTestCase {
Mockery context = new Mockery();
public void testCalcService() {
// set up
final ICalculatorService calcService = context
.mock(ICalculatorService.class);
final int assumedResult = 2;
// expectations
context.checking(new Expectations() {
{
oneOf(calcService).add(1, 1);
will(returnValue(assumedResult));
}
});
System.out.println(calcService.add(1, 1));
}
}
|
在 jMock 中,開發人員可以按照下面的語法定義預期值,從而實現更復雜的應用。例如我們可以模擬底層驅動程式的輸出,在上層應用程式中使用這些模擬資料。具體可以參考 jMock 的官方網站。
1 2 3 4 5 |
invocation-count (mock-object).method(argument-constraints);
inSequence(sequence-name);
when(state-machine.is(state-name));
will(action);
then(state-machine.is(new-state-name));
|
EasyMock 框架介紹
在實際開發中,不少開發人員也使用 EasyMock 來進行測試驅動開發。 EasyMock 具有以下的特點
- 在執行時 (runtime) 改變方法名或引數順序,測試程式碼不會破壞;
- 支援返回值和異常;
- 對於一個或多個虛擬物件,支援檢查方法呼叫次序;
- 只支援 Java 5.0 及以上版本;
與大多數 MOCK 框架一樣,我們可以在 IDE 中使用並進行開發。本文以最常用的 Eclipse 為例。
下載 EasyMock
在 EasyMock 官方網站,我們可以下載當前穩定版本 EasyMock2.4 。
配置類路徑
為了使用 EasyMock 2.4,您需要加入下面的 JAR 檔案到當前的類路徑。
- easymock.jar
圖 2. 已新增到 TestEasyMock 專案中 EasyMock 的 JAR 檔案
使用 EasyMock 模擬介面
清單 5. ILEDCard 程式碼
1 2 3 4 5 |
public interface ILEDCard {
String getMessage();
void setMessage(String message);
}
|
清單 6. LED 程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class LED {
private ILEDCard ledCard;
public LED(ILEDCard ledCard) {
this.ledCard = ledCard;
}
public String ShowMesage() {
return this.ledCard.getMessage();
}
public void setMessage(String message) {
this.ledCard.setMessage(message);
}
}
|
我們首先建立一個 Mock 的物件 mockLEDCard 來代表 LED 卡的行為,並初始化 LED 物件。
1 2 3 4 5 |
protected void setUp() throws Exception {
super.setUp();
mockLEDCard = createMock(ILEDCard.class);
led = new LED(mockLEDCard);
}
|
之後我們對 ShowMessage 方法進行測試。
1 2 3 4 5 6 7 |
public void testGetWord() {
expect(mockLEDCard.getMessage()).andReturn("This is a EasyMock Test!");
replay(mockLEDCard);
led.ShowMesage();
verify(mockLEDCard);
}
|
清單 7 顯示了完整的程式碼。
清單 7. AEasyMockTestCase 程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
import static org.easymock.EasyMock.*;
import junit.framework.TestCase;
public class AEasyMockTestCase extends TestCase {
private LED led;
private ILEDCard mockLEDCard;
protected void setUp() throws Exception {
super.setUp();
mockLEDCard = createMock(ILEDCard.class);
led = new LED(mockLEDCard);
}
protected void tearDown() throws Exception {
super.tearDown();
}
public void testGetWord() {
expect(mockLEDCard.getMessage()).andReturn("This is a EasyMock Test!");
replay(mockLEDCard);
led.ShowMesage();
verify(mockLEDCard);
}
public void testSetWord() {
mockLEDCard.setMessage("Another test");
replay(mockLEDCard);
led.setMessage("Another test");
verify(mockLEDCard);
}
}
|
通過上文對 jMock 和 EasyMock 的介紹,我們可以發現 jMock 可以靈活的定義物件的行為。例如 mock.expects(once()).method("method2").with( same(b1), ANYTHING ).will(returnValue(method2Result)); 這點在 EasyMock 裡比較難於實現。
Rmock 及其它
目前比較流行的 mock 工具,還有 RMock, 目前的版本的是 2.0,當使用 jUnit 開發測試用例時,它支援設定-修改-執行-驗證這樣的工作流。它加強了基於互動和基於狀態的測試,同時有更好的測試工作流定義。 Rmock 還可以使用 DynamicSuite 來解決維護 TestSuites 的問題。
市場上還有支援各種語言的 Mock object 的框架,如 pMock(Python),NMockLib(C#),Mocha(Ruby),JSMock(JavaScript),mockpp(C++) 。
結語
在軟體開發過程中,開發人員需要注重測試驅動開發,並利用模擬物件的技術來幫助進行測試。許多開發人員不習慣於頻繁編寫測試。即使需要編寫測試,通常都是簡單的進行主要功能測試。如果要測試程式碼的某些難以到達的部分,選擇各種 Mock object 的框架可以降低開發測試的複雜度。
同時在硬體相關的程式開發中尤其是驅動開發,應用 Mock 技術將極大地提高軟體開發的進度,並且減少程式碼中的缺陷,大大提高硬體軟體相容性和可靠性。
相關主題
- “架構宣言:採用敏捷開發,第 1 部分”(developerWorks,2008 年 6 月):您可以通過本文了什麼是解敏捷開發,如何利用其優點,以及對實現敏捷流程和架構對組織的要求。
- “developerWorks 訪談:Scott Ambler 談敏捷開發”(developerWorks,2008 年 7 月):IBM 敏捷開發實踐領導者解釋敏捷開發及其業務例項,並且澄清處理過程中的一些真相。
- “EasyMock 使用方法與原理剖析”(developerWorks,2007 年 10 月):本文將對 EasyMock 的功能和原理進行介紹,並通過示例來說明如何使用 EasyMock 進行單元測試。
- “使用模仿物件進行單元測試”(developerWorks,2003 年 3 月):本文將演示一種重構技術,該技術根據工廠方法設計模式來建立模仿物件。
- 若要獲得 jMock 的副本,您可以從 jMock 下載某個版本。
- 若要獲得 EasyMock 的副本,您可以從 EasyMock 下載某個版本。
from: https://www.ibm.com/developerworks/cn/java/j-lo-mockobject/index.html