單元測試基本概念
本主題描述在 Visual Studio 測試總管撰寫和執行單元測試的基本概念。它包含以下各節:
單元測試概觀
- 快速入門
MyBank 方案範例
建立單元測試專案
撰寫您的測試
在 [測試總管] 中執行測試
從 [測試總管] 工具列執行和檢視測試
在每次建置之後執行測試
篩選和分組測試清單
偵錯單元測試
單元測試的其他工具
從測試產生應用程式程式碼
使用資料驅動型測試方法產生多個測試
分析單元測試程式碼涵蓋範圍
隔離與 Microsoft Fakes 的單元測試方法
單元測試概觀
Visual Studio 測試總管的設計可支援開發人員和小組在其軟體開發實務中併入「單元測試」(unit testing)。單元測試可協助您驗證應用程式程式碼如您預期地執行,藉此確保程式的正確性。在單元測試中,您可以分析程式功能,找出離散的可測試行為,也就是能做為個別單位來測試的行為。您可以使用「單元測試架構」(unit testing framework) 來建立這些行為的測試並報告測試結果。
當單元測試是軟體開發工作流程中不可或缺的一部分時,就能發揮最大的作用。一旦您撰寫函式或其他應用程式程式碼區塊,就會建立單元測試,以便驗證該程式碼的行為是否對應於輸入資料的標準、界限和不正確情況,並驗證程式碼所做的任何明確或隱含假設。在所謂「測試驅動式開發」(test driven development) 的軟體開發實務中,您須在撰寫程式碼之前先建立單元測試,以便將單元測試做為該功能的設計文件和功能規格。
[測試總管] 提供富彈性又有效率的方式來執行單元測試,並可在 Visual Studio 中檢視其結果。Visual Studio 會安裝 Managed 程式碼和原生程式碼適用的 Microsoft 單元測試架構。[測試總管] 也可以執行在其中已實作 [測試總管] 附加元件介面的協力廠商和開放原始碼的單元測試架構。您可以透過 Visual Studio 擴充功能管理員和 Visual Studio 組件庫加入多個這些架構。請參閱如何:安裝協力廠商單元測試架構
[測試總管] 檢視可以顯示所有測試,或已通過、失敗、尚未執行或略過的測試。在搜尋方塊中找出全域層級中相符的文字或選取其中一個預先定義的篩選器,即可篩選任何檢視中的測試。您可以隨時執行測試的任何選取範圍。使用 Visual Studio Ultimate 時,可以在每次建置之後自動執行測試。測試回合的結果會立即顯示在 [總管] 視窗上方的通過/失敗列中。選取測試時,會顯示測試方法結果的詳細資料。
快速入門
如需單元測試的簡介以便直接參考編碼,請參閱下列其中一個主題:
MyBank 方案範例
在本主題中,我們會使用稱為 MyBank 的虛構應用程式開發來做為範例。您不需要實際程式碼來照著本主題中的說明進行。測試方法會以 C# 撰寫,並使用 Managed 程式碼適用的 Microsoft 單元測試架構來呈現,不過,這個概念可輕鬆地轉移到其他語言和架構。
我們第一次在 MyBank 應用程式的設計嘗試包括帳戶元件 (代表個別帳戶及其與銀行的交易),和資料庫元件 (代表可彙總及管理個別帳戶的功能)。
我們建立的 MyBank 方案包含兩個專案:
Accounts
BankDb
我們第一次在 Accounts 專案的設計嘗試包含一個保留帳戶相關基本資訊的類別、一個指定任一種帳戶通用功能 (例如在帳戶的資產中存款和提款) 的介面,以及一個從該介面衍生代表支票帳戶的類別。一開始我們先在帳戶專案中建立下列原始程式檔:
AccountInfo.cs 定義帳戶的基本資訊。
IAccount.cs 定義帳戶的標準 IAccount 介面,包括從帳戶資產存款和提款和擷取帳戶餘額的方法。
CheckingAccount.cs 包含的 CheckingAccount 類別可實作支票帳戶的 IAccounts 介面。
我們從經驗中知道一件事,那就是從支票帳戶提款必須先確認提取的金額小於帳戶餘額。因此我們使用一個可檢查此條件的方法來覆寫 CheckingAccount 中的 IAccount.Withdaw 方法。此方法可能看起來像這樣:
public void Withdraw(double amount)
{
if(m_balance >= amount)
{
m_balance -= amount;
}
else
{
throw new ArgumentException(amount, "Withdrawal exceeds balance!")
}
}
現在我們已經有一些程式碼,該開始測試了。
建立單元測試專案
單元測試專案通常會反映單一程式碼專案的結構。在 MyBank 範例中,您可以將名為 AccountsTests 和 BankDbTests 的兩個單元測試專案加入至 MyBanks 方案中。這些測試專案名稱可隨意命名,不過最好採用標準命名慣例。
將單元測試專案加入至方案:
在 [檔案] 功能表上,選擇 [新增],然後選擇 [專案] (鍵盤 Ctrl + Shift + N)。
在 [新增專案] 對話方塊中,展開 [已安裝] 節點,選擇您想要用於測試專案的語言,然後選擇 [測試]。
若要使用其中一個 Microsoft 單元測試架構,請從專案範本清單中選擇 [單元測試專案]。否則,請選擇您所要使用單元測試架構的專案範本。若要測試本例的 Accounts 專案,請將專案命名為 AccountsTests。
警告 並非所有協力廠商和開放原始碼的單元測試架構都提供 Visual Studio 專案範本。如需建立專案的相關資訊,請參閱架構文件。
在您的單元測試專案中,可在本例中將受測程式碼專案的參考加入至帳戶專案。
建立程式碼專案的參考:
在 [方案總管] 中選取專案。
在 [專案] 功能表上,選擇 [加入參考]。
在 [參考管理員] 對話方塊上,開啟 [方案] 節點,然後選擇 [專案]。選取程式碼專案名稱,然後關閉對話方塊。
每個單元測試專案包含的類別都可反映程式碼專案中的類別名稱。在本例中,AccountsTests 專案可能包含下列類別:
AccountInfoTests 類別包含 BankAccount 專案中 AccountInfo 類別的單元測試方法。
CheckingAccountTests 類別包含 CheckingAccount 類別的單元測試方法。
撰寫您的測試
您使用的單元測試架構和 Visual Studio IntelliSense 會引導您完成建立程式碼專案的單元測試。若要在 [測試總管] 中執行,大部分的架構都會要求您加入特定屬性,以識別單元測試方法。這些架構也會提供一個辨別測試方法是否通過或失敗的方式,通常是透過判斷提示陳述式或方法屬性。其他屬性會識別在類別初始化和每個測試方法之前的選用設定方法,和識別在每個測試方法之後和終結類別之前的清除方法。
AAA (排列、作用、判斷提示) 模式是為受測方法撰寫單元測試的常見方式。
單元測試方法的 [排列] 區段會初始化物件,並為傳遞至受測方法的資料設定值。
[作用] 區段會叫用含有所排列參數的受測方法。
[判斷提示] 區段會驗證受測方法的動作是否如預期。
若要測試本例的 CheckingAccount.Withdraw 方法,我們可以撰寫兩個測試:一個是驗證方法的標準行為,另一個則是驗證提款金額超過餘額將會失敗。在 CheckingAccountTests 類別中,我們會加入下列方法:
[TestMethod]
public void Withdraw_ValidAmount_ChangesBalance()
{
// arrange
double currentBalance = 10.0;
double withdrawal = 1.0;
double expected = 9.0;
var account = new CheckingAccount("JohnDoe", currentBalance);
// act
account.Withdraw(withdrawal);
double actual = account.Balance;
// assert
Assert.AreEqual(expected, actual);
}
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void Withdraw_AmountMoreThanBalance_Throws()
{
// arrange
var account = new CheckingAccount("John Doe", 10.0);
// act
account.Withdraw(1.0);
// assert is handled by the ExpectedException
}
請注意,Withdraw_ValidAmount_ChangesBalance 會使用明確的 Assert 陳述式來判斷測試方法是否通過或失敗,而 Withdraw_AmountMoreThanBalance_Throws 則會使用 ExpectedException 屬性來判斷測試方法是否成功。實際上,單元測試架構會將測試方法包在 try/catch 陳述式中。在大部分情況下,如果攔截到例外狀況時,測試方法便會失敗,並忽略例外狀況。如果指定的例外狀況擲回,則 ExpectedException 屬性就會導致測試方法通過。
如需 Microsoft 單元測試架構的詳細資訊,請參閱下列其中一個主題:
設定逾時
在個別的測試方法上設定逾時:
[TestMethod]
[Timeout(2000)] // Milliseconds
public void My_Test()
{ ...
}
設定允許逾時上限:
[TestMethod]
[Timeout(TestTimeout.Infinite)] // Milliseconds
public void My_Test ()
{ ...
}
在 [測試總管] 中執行測試
在建置測試專案後,這些測試便會出現在 [測試總管] 中。如果沒有看到 [測試總管],請選擇 Visual Studio 功能表上的 [測試],接著選擇 [Windows],然後選擇 [測試總管]。
隨著您執行、撰寫及重新執行測試,[測試總管] 的預設檢視便會顯示 [失敗的測試]、[通過的測試]、[略過的測試] 及 [未執行的測試] 群組中的結果。您可以選擇群組標題,以開啟顯示該群組中所有這些測試的檢視。
從 [測試總管] 工具列執行和檢視測試
[測試總管] 工具列可協助您探索、組織和執行您有興趣的測試。
您可以選擇 [全部執行] 以執行所有測試,或選擇 [執行] 以選擇要執行的一小組測試。執行一組測試之後,測試回合的摘要會出現在 [測試總管] 視窗的底部。在底部窗格中選取某個測試以檢視該測試的詳細資料。從內容功能表中選擇 [開啟測試] (鍵盤:F12) 以顯示所選測試的原始程式碼。
在每次建置之後執行測試
警告 |
---|
只有 Visual Studio Ultimate 支援在每次建置之後執行單元測試。 |
若要在每次本機組建之後執行單元測試,請選擇標準功能表上的 [測試],接著在 [測試總管] 工具列上選擇 [建置之後執行測試]。 |
篩選和分組測試清單
若有大量測試,您可以在 [測試總管] 搜尋方塊中輸入文字以便依指定字串篩選清單。您可以從篩選清單中選擇以進一步限制篩選事件。
若要依分類將測試分組,請選擇 [群組依據] 按鈕。 |
如需詳細資訊,請參閱使用測試總管執行單元測試。
偵錯單元測試
您可以使用 [測試總管] 來啟動測試的偵錯工作階段。使用 Visual Studio 偵錯工具逐步執行程式碼可讓您順暢地在單元測試和受測專案之間來回進行。啟動偵錯:
在 Visual Studio 編輯器中,於您要偵錯的一個或多個測試方法中設定中斷點。
注意事項 由於測試方法可以依照任何順序執行,請在您要偵錯的所有測試方法中設定中斷點。
在 [測試總管] 中選取測試方法,然後從捷徑功能表中選擇 [偵錯所選測試] 。
如需偵錯工具的詳細資訊,請參閱Visual Studio 偵錯。
單元測試的其他工具
從測試產生應用程式程式碼
如果您在撰寫專案程式碼之前先撰寫測試,則可以使用 IntelliSense 在專案程式碼中產生類別和方法。在測試方法中撰寫可呼叫所要產生類別或方法的陳述式,然後開啟該呼叫下的 IntelliSense 功能表。如果呼叫的是新類別的建構函式,請從功能表中選擇 [產生新的型別],然後遵循精靈以將類別插入程式碼專案中。如果呼叫的是方法,請從 IntelliSense 功能表中選擇 [產生新的方法]。
使用資料驅動型測試方法產生多個測試
注意事項 |
---|
這些程序只適用於透過 Managed 程式碼適用的 Microsoft 單元測試架構所撰寫的測試方法。如果您使用不同的架構,請參閱架構文件的同等功能。 |
「資料驅動型測試方法」(Data-driven test method) 可讓您驗證單一單元測試方法中某個範圍的值。若要建立資料驅動型單元測試方法,請以 DataSource 屬性來裝飾方法,以便使用該屬性來指定資料來源和資料表,該資料表會包含您要測試的變數值。在方法主體中,您可使用 TestContext.DataRow[ColumnName] 索引子將資料列值指派給變數。
例如,假設我們將不需要的方法加入至名為 AddIntegerHelper 的 CheckingAccount 類別。AddIntegerHelper 會加入兩個整數。
若要為 AddIntegerHelper 方法建立資料驅動型測試,我們會先建立名為 AccountsTest.accdb 的 Access 資料庫和名為 AddIntegerHelperData 的資料表。AddIntegerHelperData 資料表會定義可指定加法第一個和第二個運算元的資料行,和可指定預期結果的資料行。我們在多個資料列中填入適當的值。
[DataSource(
@"Provider=Microsoft.ACE.OLEDB.12.0;Data Source=C:\Projects\MyBank\TestData\AccountsTest.accdb",
"AddIntegerHelperData"
)]
[TestMethod()]
public void AddIntegerHelper_DataDrivenValues_AllShouldPass()
{
var target = new CheckingAccount();
int x = Convert.ToInt32(TestContext.DataRow["FirstNumber"]);
int y = Convert.ToInt32(TestContext.DataRow["SecondNumber"]);
int expected = Convert.ToInt32(TestContext.DataRow["Sum"]);
int actual = target.AddIntegerHelper(x, y);
Assert.AreEqual(expected, actual);
}
資料表中一個資料列執行一次屬性的方法。如果任何反覆項目失敗,[測試總管] 會報告該方法的測試失敗。該方法的測試結果詳細資料窗格會向您顯示每個資料列通過/失敗狀態的方法。
如需詳細資訊,請參閱如何:建立資料驅動型單元測試。
分析單元測試程式碼涵蓋範圍
注意事項 |
---|
單元測試程式碼涵蓋範圍適用於原生和 Managed 語言,以及可由單元測試架構執行的所有單元測試架構。 |
您可以使用 Visual Studio 程式碼涵蓋範圍工具來判斷您的單元測試實際測試的產品程式碼數量。您可以在方案中的所選測試或所有測試上執行程式碼涵蓋範圍。[程式碼涵蓋範圍結果] 視窗會顯示線條、函式、類別、命名空間及模組所運用的產品程式碼區塊的百分比。
若要在方案中執行測試方法的程式碼涵蓋範圍,
請選擇 [Visual Studio] 功能表上的 [測試],然後選擇 [分析程式碼涵蓋範圍]。
選擇下列其中一個命令:
[選取的測試] 會執行您在 [測試總管] 中所選取的測試方法。
[所有測試] 會執行方案中的所有測試方法。
涵蓋範圍結果會出現在 [程式碼涵蓋範圍結果] 視窗中。
如需詳細資訊,請參閱使用程式碼涵蓋範圍來決定所測試的程式碼數量。
隔離與 Microsoft Fakes 的單元測試方法
注意事項 |
---|
Microsoft Fakes 僅適用於 Visual Studio Ultimate。Microsoft Fakes 只能用於透過 Managed 程式碼適用的單元測試架構所撰寫的測試方法。 |
問題
若受測方法呼叫會導入外部相依性的函式,則專門驗證函式中內部程式碼的測試方法可能會難以撰寫。例如,CheckingAccount 範例類別的方法可能應呼叫 BankDb 元件才能更新主要資料庫。我們可以重構 CheckingAccount 類別,以便看起來如下所示:
class CheckingAccount : IAccount
{
public CheckingAccount(customerName, double startingBalance, IBankDb bankDb)
{
m_bankDb = bankDb;
// set up account
}
public void Withdraw(double amount)
{
if(m_balance >= amount)
{
m_balance = m_MyBankDb.Withdraw(m_accountInfo.ID, amount);
}
else
{
throw new ArgumentException(amount, "Withdrawal exceeds balance!")
}
}
private IBankDb m_bankDb = null;
// ...
由於呼叫 m_bankDb.Withdraw 所造成的問題,現在此 CheckingAccount.Withdraw 方法的單元測試都會失敗。可能會遺失資料庫或網路連線,或資料庫上的權限可能會出錯。m_bankDB.Withdraw 呼叫中的失敗可能導致測試失敗,原因則與其內部的程式碼無關。
Microsoft Fakes 方案
Microsoft Fakes 會建立包含類別和方法的組件,您可以用該組件來替代單元測試方法中會造成相依性的類別。替代類別會在所產生的 Fakes 模組中為目標元件中的每個公用方法宣告一種方法和一種委派。在測試方法中,您可以實作該項委派,以便在您要測試的方法中建立確切的相依性呼叫行為。
在本例中,我們可以為 BankDb 專案建立 Fakes 組件,然後使用 Fakes 所產生和自 IBankDb 介面衍生的 StubIBankDb 類別,來移除與資料庫互動所造成的不確定性。Withdraw_ValidAmount_ChangesBalance 測試方法的修訂版本接著會看起來像這樣:
[TestMethod]
public void Withdraw_ValidAmount_ChangesBalance()
{
// arrange
double currentBalance = 10.0;
double withdrawal = 1.0;
double expected = 9.0;
// set up the Fakes object and delegate
var stubBankDb = new MyBank.Stubs.StubIBankDb();
stubBankDb.WithdrawDoubleDouble = (id, amount) => { return 9.0; }
var account = new CheckingAccount("JohnDoe", currentBalance, stubBankDb);
// act
account.Withdraw(withdrawal);
double actual = account.Balance;
// assert
Assert.AreEqual(expected, actual);
}
測試方法中的這一行:
stubBankDb.WithdrawDoubleDouble = (id, amount) => { return 9.0; }
會使用 lambda 運算式為 Withdraw 方法實作 Fakes 委派。stubBankDb.Withdraw 方法會呼叫這項委派,因此一律會傳回指定的數量,讓測試方法能確實地驗證 Accounts 方法的行為。
進一步了解 Microsoft Fakes
Microsoft Fakes 會使用兩種方法來建立替代類別:
「虛設常式」(Stub) 會產生自目標相依性類別的父介面衍生的替代類別。虛設常式方法可以取代為目標類別的公用虛擬方法。
「填充碼」(Shim) 會使用執行階段檢測將呼叫轉向至目標方法,以使用填充碼方法來替代非虛擬方法。
在這兩種方法中,您可以對相依性方法使用呼叫所產生的委派,以指定您要讓測試方法執行的行為。
如需詳細資訊,請參閱使用 Microsoft Fakes 在測試期間隔離程式碼。