逐步解說:為受控碼建立和執行單元測試

本文會引導您使用適用於受控碼的 Microsoft 單元測試架構和 Visual Studio [測試總管],來建立、執行和自訂一系列的單元測試。 您可以從開發中的 C# 專案開始,建立執行其程式碼的測試、執行測試,並檢查結果。 然後,變更專案程式碼並重新執行測試。 如果您想要在執行這些步驟之前了解這些工作的概念,請參閱單元測試基本概念

建立要測試的專案

  1. 開啟 Visual Studio。

  2. 在開始視窗中,選擇 [建立新專案]

  3. 搜尋並選取 C# [主控台應用程式] 專案範本 (針對 .NET),然後按 [下一步]

    注意

    如果您未看到 [主控台應用程式] 範本,您可以從 [建立新專案] 視窗中安裝。 在 [找不到你要尋找的項目嗎?] 訊息中,選擇 [安裝更多工具和功能] 連結。 然後,在 Visual Studio 安裝程式中選擇 [.NET 桌面開發] 工作負載。

  4. 將專案命名為 Bank,然後按 [下一步]

    選擇建議的目標 Framework 或 .NET 8,然後選擇 [建立]

    即會建立 Bank 專案並顯示在 [方案總管] 中,並於程式碼編輯器中開啟 Program.cs 檔案。

    注意

    如果 Program.cs 檔案並未在編輯器中開啟,請在 [方案總管] 中按兩下 Program.cs 檔案來開啟。

  5. 使用下列會定義 BankAccount 類別的 C# 程式碼取代 Program.cs 內容:

    using System;
    
    namespace BankAccountNS
    {
        /// <summary>
        /// Bank account demo class.
        /// </summary>
        public class BankAccount
        {
            private readonly string m_customerName;
            private double m_balance;
    
            private BankAccount() { }
    
            public BankAccount(string customerName, double balance)
            {
                m_customerName = customerName;
                m_balance = balance;
            }
    
            public string CustomerName
            {
                get { return m_customerName; }
            }
    
            public double Balance
            {
                get { return m_balance; }
            }
    
            public void Debit(double amount)
            {
                if (amount > m_balance)
                {
                    throw new ArgumentOutOfRangeException("amount");
                }
    
                if (amount < 0)
                {
                    throw new ArgumentOutOfRangeException("amount");
                }
    
                m_balance += amount; // intentionally incorrect code
            }
    
            public void Credit(double amount)
            {
                if (amount < 0)
                {
                    throw new ArgumentOutOfRangeException("amount");
                }
    
                m_balance += amount;
            }
    
            public static void Main()
            {
                BankAccount ba = new BankAccount("Mr. Bryan Walton", 11.99);
    
                ba.Credit(5.77);
                ba.Debit(11.22);
                Console.WriteLine("Current balance is ${0}", ba.Balance);
            }
        }
    }
    
  6. 按一下滑鼠右鍵,選擇 [方案總管] 中的 [重新命名],將檔案重新命名為 BankAccount.cs

  7. 在 [建置] 功能表上按一下 [建置解決方案] (或按 Ctrl + SHIFT + B)。

您現在已擁有具備測試方法的專案。 本文中的測試著重於 Debit 方法。 從帳戶中提領貨幣時,會呼叫 Debit 方法。

建立單元測試專案

  1. 在 [檔案] 功能表上,選取 [新增]>[新增專案]

    提示

    您也可以在 [方案總管] 中以滑鼠右鍵按一下方案,然後選擇 [新增]>[新增專案]

  2. 在搜尋方塊中輸入測試,選取 C# 作為語言,並針對 .NET 範本選取 C# [MSTest 單元測試專案],然後按 [下一步]

    注意

    在 Visual Studio 2019 16.9 版中,MSTest 專案範本為 [單元測試專案]

  3. 將專案命名為 BankTests,然後按 [下一步]

  4. 選擇建議的目標 Framework 或 .NET 8,然後選擇 [建立]

    BankTests 專案就會新增至 Bank 方案中。

  5. BankTests 專案中,新增 Bank 專案的參考。

    在 [方案總管] 中,選取 BankTests 專案底下的 [相依性],然後從右鍵功能表中選擇 [新增參考] (或 [新增專案參考])。

  6. 在 [參考管理員] 對話方塊中,展開 [專案] 並選取 [方案],然後選取 [Bank] 項目。

  7. 選擇確定

建立測試類別

建立測試類別,以確認 BankAccount 類別。 您可以使用由專案範本所產生的 UnitTest1.cs 檔案;不過,請使用更具有描述性的名稱來命名檔案和類別。

重新命名檔案和類別

  1. 若要重新命名檔案,請在 [方案總管] 中選取 BankTests 專案中的 UnitTest1.cs 檔案。 從右鍵功能表中選擇 [重新命名] (或按 F2),然後將檔案重新命名為 BankAccountTests.cs

  2. 若要重新命名類別,請將游標放在程式碼編輯器中的 UnitTest1、按一下滑鼠右鍵,然後選擇 [重新命名] (或按 F2)。 鍵入 BankAccountTests,然後按下 Enter

BankAccountTests.cs 檔案現在會包含下面程式碼:

// The 'using' statement for Test Tools is in GlobalUsings.cs
// using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace BankTests
{
    [TestClass]
    public class BankAccountTests
    {
        [TestMethod]
        public void TestMethod1()
        {
        }
    }
}

新增 using 陳述式

using 陳述式新增至測試類別,以便能夠呼叫受測專案,而不需使用完整限定名稱。 在類別檔案的頂端,加入:

using BankAccountNS;

測試類別需求

測試類別的最低需求如下:

  • 對於包含要在 [測試總管] 中執行之單元測試方法的任何類別而言,[TestClass] 屬性是必要的。

  • 您要讓 [測試總管] 辨識的每個測試方法都必須具有 [TestMethod] 屬性。

單元測試專案中可以含有不具有 [TestClass] 屬性的其他類別,而測試類別中也可以含有不具有 [TestMethod] 屬性的其他方法。 您可以從您的測試方法中呼叫這些其他類別和方法。

建立第一個測試方法

在此程序中,您會撰寫單元測試方法以驗證 BankAccount 類別之 Debit 方法的行為。

至少有三項需要檢查的行為:

提示

您可以刪除預設的 TestMethod1 方法,因為您不會在本逐步解說中使用它。

建立測試方法

第一個測試會確認有效金額 (即小於帳戶餘額但大於零的金額) 將從帳戶中提領正確的金額。 將下面方法加入至該 BankAccountTests 類別:

[TestMethod]
public void Debit_WithValidAmount_UpdatesBalance()
{
    // Arrange
    double beginningBalance = 11.99;
    double debitAmount = 4.55;
    double expected = 7.44;
    BankAccount account = new BankAccount("Mr. Bryan Walton", beginningBalance);

    // Act
    account.Debit(debitAmount);

    // Assert
    double actual = account.Balance;
    Assert.AreEqual(expected, actual, 0.001, "Account not debited correctly");
}

此方法很直接:會設定具有初始餘額的新 BankAccount 物件,然後提領有效的金額。 它會使用 Assert.AreEqual 方法,以確認結尾餘額如預期。 Assert.AreEqualAssert.IsTrue 等方法經常用於單元測試中。 如需撰寫單元測試的詳細資訊,請參閱撰寫測試

測試方法需求

測試方法必須符合下列需求:

  • 它是以 [TestMethod] 屬性裝飾。

  • 它會傳回 void

  • 它不能有參數。

建置並執行測試

  1. 在 [建置] 功能表上選擇 [建置解決方案] (或按 Ctrl + SHIFT + B)。

  2. 如果 [測試總管] 未開啟,請從頂端功能表列選擇 [測試]>[測試總管] (或 [測試]>[Windows]>[測試總管]) 來開啟 (或按 Ctrl + ET)。

  3. 選擇 [全部執行] 以執行測試 (或按 Ctrl + RV)。

    執行測試時,[測試總管] 視窗頂端的狀態列會顯示動畫效果。 在測試回合結束時,如果所有的測試方法都成功,狀態列會變成綠色,如果有任何測試失敗則變成紅色。

    在本案例中,測試會失敗。

  4. 在 [測試總管] 中選取該方法,以在視窗底部檢視詳細資料。

修正程式碼並重新執行測試

測試結果會包含說明失敗的訊息。 您可能需要向下切入才能看到此訊息。 針對 AreEqual 方法,訊息會顯示預期項目和實際收到的項目。 您預期餘額會減少,但它增加了提領的金額。

單元測試發現了一個錯誤:提領的金額應該從帳戶餘額「減去」,但卻被「加入」至帳戶餘額。

修正 Bug

若要更正錯誤,請在 BankAccount.cs 檔案中取代這一行:

m_balance += amount;

取代為:

m_balance -= amount;

重新執行測試

在 [測試總管] 中,選擇 [全部執行] 以重新執行測試 (或按 Ctrl + RV)。 紅色/綠色狀態列會變成綠色,表示測試已通過。

Test Explorer in Visual Studio 2019 showing passed test

Test Explorer in Visual Studio 2019 showing passed test

使用單元測試改善您的程式碼

這一節會說明分析、單元測試開發和重構的反覆流程,是如何協助您讓生產環境程式碼更加強固而有效。

分析問題

您已建立測試方法,以確認會在 Debit 方法中正確扣除有效金額。 現在,請確認如果付款金額處於下列任一狀況,該方法會擲回 ArgumentOutOfRangeException

  • 大於餘額,或
  • 小於零。

建立並執行新的測試方法

建立測試方法,以確認付款金額小於零時的正確行為:

[TestMethod]
public void Debit_WhenAmountIsLessThanZero_ShouldThrowArgumentOutOfRange()
{
    // Arrange
    double beginningBalance = 11.99;
    double debitAmount = -100.00;
    BankAccount account = new BankAccount("Mr. Bryan Walton", beginningBalance);

    // Act and assert
    Assert.ThrowsException<System.ArgumentOutOfRangeException>(() => account.Debit(debitAmount));
}

使用 ThrowsException 方法來判斷提示已擲回正確的例外狀況。 除非擲回 ArgumentOutOfRangeException,否則此方法會造成測試失敗。 如果在付款金額小於零時,暫時修改受測方法以擲回多個泛型 ApplicationException,則測試會正確運作 — 即測試會失敗。

若要測試提領金額大於餘額的案例,請執行下列步驟:

  1. 建立名為 Debit_WhenAmountIsMoreThanBalance_ShouldThrowArgumentOutOfRange的新測試方法。

  2. Debit_WhenAmountIsLessThanZero_ShouldThrowArgumentOutOfRange 中的方法主體複製到新的方法。

  3. debitAmount 設定為大於餘額的數字。

執行兩個測試,並確認是否通過。

繼續分析

您可以進一步改善要測試的方法。 透過目前的實作,我們沒有辦法知道哪些條件 (amount > m_balanceamount < 0) 會導致測試期間擲回例外狀況。 我們只知道在方法中的某處擲回 ArgumentOutOfRangeException。 如果我們可以分辨 BankAccount.Debit 中的哪個條件造成擲回例外狀況 (amount > m_balanceamount < 0),讓我們可以確信我們的方法正確地對其引數進行了例行性檢查,這樣會比較好。

請再次查看受測方法 (BankAccount.Debit),您會注意到兩個條件陳述式都使用只接受引數名稱作為參數的 ArgumentOutOfRangeException 建構函式:

throw new ArgumentOutOfRangeException("amount");

有一個建構函式可用來報告更豐富的資訊:ArgumentOutOfRangeException(String, Object, String) 包含引數的名稱、引數值和使用者定義的訊息。 您可以重構受測方法以使用這個建構函式。 更好的是,您可以使用可公開取得的類型成員來指定錯誤。

重構受測程式碼

首先,在類別範圍定義錯誤訊息的兩個常數。 將定義放在受測類別中,BankAccount

public const string DebitAmountExceedsBalanceMessage = "Debit amount exceeds balance";
public const string DebitAmountLessThanZeroMessage = "Debit amount is less than zero";

然後,修改 Debit 方法中的兩個條件陳述式:

if (amount > m_balance)
{
    throw new System.ArgumentOutOfRangeException("amount", amount, DebitAmountExceedsBalanceMessage);
}

if (amount < 0)
{
    throw new System.ArgumentOutOfRangeException("amount", amount, DebitAmountLessThanZeroMessage);
}

重構測試方法

透過移除對 Assert.ThrowsException 的呼叫來重構測試方法。 將呼叫包裝到 Debit()try/catch 區塊中、攔截預期的特定例外狀況,並確認其相關聯的訊息。 Microsoft.VisualStudio.TestTools.UnitTesting.StringAssert.Contains 方法可讓您比較兩個字串。

現在,Debit_WhenAmountIsMoreThanBalance_ShouldThrowArgumentOutOfRange 可能看起來像這樣:

[TestMethod]
public void Debit_WhenAmountIsMoreThanBalance_ShouldThrowArgumentOutOfRange()
{
    // Arrange
    double beginningBalance = 11.99;
    double debitAmount = 20.0;
    BankAccount account = new BankAccount("Mr. Bryan Walton", beginningBalance);

    // Act
    try
    {
        account.Debit(debitAmount);
    }
    catch (System.ArgumentOutOfRangeException e)
    {
        // Assert
        StringAssert.Contains(e.Message, BankAccount.DebitAmountExceedsBalanceMessage);
    }
}

重新測試、重新撰寫和重新分析

目前,測試方法不會處理其應處理的所有案例。 如果受測方法 (Debit 方法) 在 debitAmount 大於餘額 (或小於零) 時未能擲回 ArgumentOutOfRangeException,則測試方法會通過。 此案例並不理想,因為您要的是測試方法在未擲回例外狀況時失敗。

此結果是測試方法中的錯誤。 若要解決這個問題,請在測試方法的結尾新增 Assert.Fail 判斷提示,以處理未擲回例外狀況的情況。

重新執行測試後顯示,測試現在會因為攔截到正確的例外狀況而「失敗」catch 區塊會攔截例外狀況,但是該方法會繼續執行,並且會在新的 Assert.Fail 判斷提示處失敗。 爲了解決此問題,請在 catch 區塊的 StringAssert 之後新增 return 陳述式。 重新執行測試即可確認您已修正這個問題。 Debit_WhenAmountIsMoreThanBalance_ShouldThrowArgumentOutOfRange 的最終版本看起來像這樣:

[TestMethod]
public void Debit_WhenAmountIsMoreThanBalance_ShouldThrowArgumentOutOfRange()
{
    // Arrange
    double beginningBalance = 11.99;
    double debitAmount = 20.0;
    BankAccount account = new BankAccount("Mr. Bryan Walton", beginningBalance);

    // Act
    try
    {
        account.Debit(debitAmount);
    }
    catch (System.ArgumentOutOfRangeException e)
    {
        // Assert
        StringAssert.Contains(e.Message, BankAccount.DebitAmountExceedsBalanceMessage);
        return;
    }

    Assert.Fail("The expected exception was not thrown.");
}

結論

測試程式碼的改善帶來了更穩固、包含更多資訊的測試方法。 但是更重要的是,它們也改善了受測程式碼。

提示

本逐步解說會使用適用於 Managed 程式碼的 Microsoft 單元測試架構。 [測試總管] 也可以從已安裝 [測試總管] 配接器的協力廠商單元測試架構來執行測試。 如需詳細資訊,請參閱安裝協力廠商單元測試架構

如需如何從命令列執行測試的資訊,請參閱 VSTest.Console.exe 命令列選項