譯者序]
縱觀軟體的開發,測試已經日益成為軟體開發過程中的重要環節,通常一個軟體的開發週期中測試要佔到一半時間甚至更多。而在測試過程中,單元測試更是萬裡長征第一步,單元測試進行得是否完善,直接影響到後期整合測試的效率。進行單元測試,有許多軟體可以自動完成,NUnit就是其中之一。這是一款與JUnit齊名的,同屬於xUnit家族的單元測試軟體(在http://www.NUnit.org我們可以免費得到這款軟體)。
[本文]
讓我們從一個簡單的例子開始。假設我們正在編寫一個銀行應用程式,而我們有一個這一領域的基本類——Account。Account支援存款、取款和資金轉帳。這個Account類看起來會是這個樣子:
namespace bank {
public class Account {
private float balance;
public void Deposit(float amount) {
balance += amount;
}
public void Withdraw(float amount) {
balance -= amount;
}
public void TransferFunds(Account destination, float amount) {
}
public float Balance {
get {
return balance;
}
}
}
}
現在我們來為這個類寫一個測試——AccountTest。我們要測試的第一個類方法是TransferFunds。
namespace bank {
using NUnit.Framework;
[TestFixture]
public class AccountTest {
[Test]
public void TransferFunds() {
Account source = new Account();
source.Deposit(200.00F);
Account destination = new Account();
destination.Deposit(150.00F);
source.TransferFunds(destination, 100.00F);
Assert.AreEqual(250.00F, destination.Balance);
Assert.AreEqual(100.00F, source.Balance);
}
}
}
首先要注意的是這個類關聯了一個[TestFixture]特性(attribute)——這表示這個類包含了測試代碼(這個特性可以被繼承)。這個類必須是公有的,但他的父類並不受限制。這個類還必須有一個預設建構函式。
類中唯一的一個方法——TransferFunds(),關聯了一個[Test]特性——這表示它是一個測試方法。測試方法的傳回值必須為void並且不能帶有參數。在我們的測試方法中,我們對被測試的對象進行了一般的初始化,執行了被測試的方法並檢查了對象的狀態。Assert類定義了一組方法用於檢查給定的條件,在我們的例子中我們使用了AreEqual()方法來確保交易過後兩個賬戶都有正確的餘額(這個方法有很多重載,我們在這個例子中使用的版本帶有兩個參數:第一個參數是我們的期望值,第二個參數是實際值)。
編譯並運行這個例子。假設你已經將你的測試代碼編譯為bank.dll。開啟NUint Gui(安裝程式會在你的案頭和“程式”菜單中建立一個捷徑),開啟GUI後,選擇File->Open功能表項目,找到你的bank.dll並在“Open”對話方塊中選中它。bank.dll裝載後你會在左邊的面板中看到一個測試樹結構,還有右邊的一組狀態面板。單擊Run按鈕,狀態條和測試樹種的TransferFunds節點變成了紅色——我們的測試失敗了。“Errors and Failures”面板顯示如下訊息——“TransferFunds: expected <250> but was <150>”,在它正下方的堆疊追蹤面板報告了測試失敗的語句在代碼中的位置——“at bank.AccountTest.TransferFunds() in C:/nunit/BankSampleTests/AccountTest.cs:line 17”
這正是預期的結果,因為我們還未實現TransferFunds()方法。現在我們來搞定它。不要關閉GUI,回到你的IDE並修改代碼,使你的TransferFunds()方法看起來像這樣:
public void TransferFunds(Account destination, float amount) {
destination.Deposit(amount);
Withdraw(amount);
}
現在重新編譯你的代碼並再次在GUI中點擊Run按鈕——狀態條和數節點變綠了。(注意GUI會自動地為你重新載入程式集;我們可以一直開著GUI而在IDE中繼續工作並寫更多的測試)。
讓我們來為我們的Account的代碼添加一些錯誤偵測。為賬戶添加一個最小餘額限制,通過你的最小透支保護費來維持它的持續運作。首先我們來為Account類添加一個最小餘額保護屬性:
private float minimumBalance = 10.00F;
public float MinimumBalance {
get {
return minimumBalance;
}
}
我們使用一個異常來指出透支:
namespace bank {
using System;
public class InsufficientFundsException : ApplicationException {
}
}
向我們的AccountTest類添加一個新的方法:
[Test]
[ExpectedException(typeof(InsufficientFundsException))]
public void TransferWithInsufficientFunds() {
Account source = new Account();
source.Deposit(200.00F);
Account destination = new Account();
destination.Deposit(150.00F);
source.TransferFunds(destination, 300.00F);
}
這個測試方法除了[Test]特性之外還關聯了一個[ExpectedException]特性——這指出測試代碼希望拋出一個指定類型的異常;如果在執行過程中沒有拋出這樣的一個異常——該測試將會失敗。編譯你的代碼並回到GUI。由於你編譯了你的測試代碼,GUI會變灰並重構了測試樹,好像這個測試還沒有被運行過(GUI可以監視測試程式集的變化,並在測試樹結構發生變化時進行更新——例如,添加了新的測試)。點擊“Run”按鈕——我們又一次得到了一個紅色的狀態條。我們得到了下面的失敗訊息:“TransferWithInsufficentFunds: InsufficientFundsException was expected”。我們來再次修改Account的代碼,象下面這樣修改TransferFunds()方法:
public void TransferFunds(Account destination, float amount) {
destination.Deposit(amount);
if(balance - amount < minimumBalance)
throw new InsufficientFundsException();
Withdraw(amount);
}
編譯並運行測試——綠了。成功!不過等等,看看我們剛寫的代碼,我們會發現銀行在每一筆不成功的轉賬操作時都虧錢了。讓我們來寫一個測試來確認我們的猜測。添加這個測試方法:
[Test]
public void TransferWithInsufficientFundsAtomicity() {
Account source = new Account();
source.Deposit(200.00F);
Account destination = new Account();
destination.Deposit(150.00F);
try {
source.TransferFunds(destination, 300.00F);
}
catch(InsufficientFundsException expected) {
}
Assert.AreEqual(200.00F,source.Balance);
Assert.AreEqual(150.00F,destination.Balance);
}
我們測試了方法的交易屬性——是否所有的操作都成功了。編譯並運行——紅條。是的,我們平白無故地損失了300塊錢——source賬戶有正確的餘額150.00,但destination賬戶顯示:$450.00。我們該如何修改?我們能夠只將最小餘額檢查的調用放到資料更新的前面麼:
public void TransferFunds(Account destination, float amount) {
if(balance - amount < minimumBalance) {
throw new InsufficientFundsException();
}
destination.Deposit(amount);
Withdraw(amount);
}
如果Withdraw()方法拋出了另外一個異常呢?我們應該在catch塊中執行一個補救處理,還是依賴我們的交易管理器來重新裝載對象的狀態?某些時候我們必須回答這樣的問題,但不是現在;可我們眼前如何應付這個失敗的測試呢——刪除它?一個不錯的方法是臨時忽略它在你的測試方法中添加下面的特性:
[Test]
[Ignore("Need to decide how to implement transaction management in the application")]
public void TransferWithInsufficientFundsAtomicity() {
// code is the same
}
編譯並運行——黃條。單擊“Test Not Run”選項卡,你會看到bank.AccountTest.TransferWithInsufficientFundsAtomicity()連同這個測試被忽略的原因一起列在列表中。
看看我們的測試代碼,我們可以看到一些適宜的重構。所有的方法共用一組公用的測試對象。讓我們來將這些初始化代碼放到一個setup方法中並在所有的測試中重用它們。我們的測試類別的重構版本像下面這樣:
namespace bank {
using System;
using NUnit.Framework;
[TestFixture]
public class AccountTest {
Account source;
Account destination;
[SetUp]
public void Init() {
source = new Account();
source.Deposit(200.00F);
destination = new Account();
destination.Deposit(150.00F);
}
[Test]
public void TransferFunds() {
source.TransferFunds(destination, 100.00f);
Assert.AreEqual(250.00F, destination.Balance);
Assert.AreEqual(100.00F, source.Balance);
}
[Test]
[ExpectedException(typeof(InsufficientFundsException))]
public void TransferWithInsufficientFunds() {
source.TransferFunds(destination, 300.00F);
}
[Test,
Ignore (
"Need to decide how to implement transaction management in the application"
)]
public void TransferWithInsufficientFundsAtomicity() {
try {
source.TransferFunds(destination, 300.00F);
}
catch(InsufficientFundsException expected) {
}
Assert.AreEqual(200.00F,source.Balance);
Assert.AreEqual(150.00F,destination.Balance);
}
}
}
注意這個初始化方法擁有通用的初始化代碼,它的傳回值類型為void,沒有參數,並且由[SetUp]特性標記。編譯並運行——同樣的黃條!
============全文完==========