MSTest 中的數據驅動測試

資料驅動測試讓你用多組輸入資料執行相同的測試方法。 與其為每個測試案例分別寫測試方法,不如先定義一次測試邏輯,並透過屬性或外部資料來源提供不同的輸入。

概觀

MSTest 提供多項資料驅動測試屬性:

Attribute 用例 適用對象
DataRow 線上測試資料 簡單、靜態的測試案例
CombinatorialData 參數值的笛卡兒積 獨立輸入的窮盡組合
DynamicData 來自方法、屬性或欄位的資料 複雜或計算測試資料
DataSource 外部資料檔案或資料庫 帶有外部資料來源的舊有情境

MSTest 也提供以下類型以擴展資料驅動情境:

  • TestDataRow<T>: 一種用於 ITestDataSource 實作(包括 DynamicData)的回傳類型,新增了顯示名稱、類別及忽略訊息等元資料支援,針對個別測試案例。
  • ITestDataSource:一個你可以在自訂屬性上實作,建立完全自訂資料來源屬性的介面。

Important

MSTest 4.4 計畫內建組合測試,僅在預覽版中提供,直到 MSTest 4.4.0 發布。 對於較早期的 MSTest 版本,請使用社群維護的 Combinatorial.MSTest 套件。

DataRowAttribute

DataRowAttribute 允許你用多個不同輸入執行同一種測試方法。 將一個或多個 DataRow 屬性套用到已使用 TestMethodAttribute 裝飾的測試方法。

引數的數目和類型必須完全符合測試方法簽章。

小提示

相關分析儀:

  • MSTEST0014 驗證參數是否 DataRow 符合測試方法的簽名。
  • MSTEST0042 偵測 DataRow 重複條目的情況,導致同一個測試案例會多次被執行。

基本用法

[TestClass]
public class CalculatorTests
{
    [TestMethod]
    [DataRow(1, 2, 3)]
    [DataRow(0, 0, 0)]
    [DataRow(-1, 1, 0)]
    [DataRow(100, 200, 300)]
    public void Add_ReturnsCorrectSum(int a, int b, int expected)
    {
        var calculator = new Calculator();
        Assert.AreEqual(expected, calculator.Add(a, b));
    }
}

支援的參數類型

DataRow 支援多種參數類型,包括原語、字串、陣列及空值:

[TestClass]
public class DataRowExamples
{
    [TestMethod]
    [DataRow(1, "message", true, 2.0)]
    public void TestWithMixedTypes(int i, string s, bool b, float f)
    {
        // Test with different primitive types
    }

    [TestMethod]
    [DataRow(new string[] { "line1", "line2" })]
    public void TestWithArray(string[] lines)
    {
        Assert.AreEqual(2, lines.Length);
    }

    [TestMethod]
    [DataRow(null)]
    public void TestWithNull(object o)
    {
        Assert.IsNull(o);
    }

    [TestMethod]
    [DataRow(new string[] { "a", "b" }, new string[] { "c", "d" })]
    public void TestWithMultipleArrays(string[] input, string[] expected)
    {
        // Starting with MSTest v3, two arrays don't need wrapping
    }
}

備註

從 MSTest v3.10 開始,使用 DateOnlyTimeOnly 作為 DataRow 的引數,並用於 DynamicData 參數化測試。

使用參數來處理可變長度引數

使用 params 關鍵字來接受可變數量的參數:

[TestClass]
public class ParamsExample
{
    [TestMethod]
    [DataRow(1, 2, 3, 4)]
    [DataRow(10, 20)]
    [DataRow(5)]
    public void TestWithParams(params int[] values)
    {
        Assert.IsTrue(values.Length > 0);
    }
}

通用測試方法

從 MSTest v3.8 開始,a TestMethod 可以有型別參數。 該框架會從 DataRowDynamicData 值推斷型別引數:

[TestMethod]
[DataRow(42)]
[DataRow("alpha")]
public void Value_RoundTrips<T>(T value)
{
    Assert.AreEqual(value, value);
}

自訂顯示名稱

設定該 DisplayName 屬性以自訂測試案例在測試檔案總管中的呈現方式:

[TestClass]
public class DisplayNameExample
{
    [TestMethod]
    [DataRow(1, 2, DisplayName = "Functional Case FC100.1")]
    [DataRow(3, 4, DisplayName = "Edge case: small numbers")]
    public void TestMethod(int i, int j)
    {
        Assert.IsTrue(i < j);
    }
}

小提示

為了更好地控制測試元資料,可以考慮使用 TestDataRow<T>DynamicDataTestDataRow<T> 支援顯示名稱與測試類別,並忽略個別測試案例的訊息。

忽略特定的測試案例

從 MSTest v3.8 開始,請使用該 IgnoreMessage 特性跳過特定資料列:

[TestClass]
public class IgnoreDataRowExample
{
    [TestMethod]
    [DataRow(1, 2)]
    [DataRow(3, 4, IgnoreMessage = "Temporarily disabled - bug #123")]
    [DataRow(5, 6)]
    public void TestMethod(int i, int j)
    {
        // Only the first and third data rows run
        // The second is skipped with the provided message
    }
}

CombinatorialDataAttribute

在 MSTest 4.4 預覽版本中,屬性 CombinatorialData 會產生每個測試方法參數值的笛卡爾積。 使用命名空間中的 Microsoft.VisualStudio.TestTools.UnitTesting.Combinatorial 型別。

為每個參數提供以下選項之一的值:

值的來源 行為
推斷值 對於 bool,使用 truefalse;對於 int,使用 01;列舉的所有已定義值;以及 null 加上從這些支援型別之一的可為 Null 版本推斷出的值。
CombinatorialValues 它會使用你傳給屬性的明確值。
CombinatorialRange 從起始值和計數,或從包含端點與步進值,產生 intuint 值。
CombinatorialRandomData 產生唯一的 int 數值。 設定 CountMinimumMaximum,並可 Seed 選擇性地控制這些數值。

以下測試結合了兩個推斷 bool 值、兩個明確值、三個範圍值及兩個種子隨機值。 MSTest 從這些獨立參數集產生 24 個測試案例。

[TestMethod]
[CombinatorialData]
public void Calculate_UsesEveryCombination(
    bool enabled,
    [CombinatorialValues(1, 3)] int factor,
    [CombinatorialRange(2, 6, 2)] int value,
    [CombinatorialRandomData(Count = 2, Minimum = 10, Maximum = 20, Seed = 42)] int offset)
{
    int result = enabled ? (factor * value) + offset : value + offset;

    Assert.IsTrue(result >= 12);
}
<TestMethod>
<CombinatorialData>
Public Sub Calculate_UsesEveryCombination(
    enabled As Boolean,
    <CombinatorialValues(1, 3)> factor As Integer,
    <CombinatorialRange(2, 6, 2)> value As Integer,
    <CombinatorialRandomData(Count:=2, Minimum:=10, Maximum:=20, Seed:=42)> offset As Integer)

    Dim result = If(enabled, (factor * value) + offset, value + offset)

    Assert.IsTrue(result >= 12)
End Sub

每個參數最多只能套用一個組合值提供者。 如果參數類型的值無法由 MSTest 推斷,請套用 CombinatorialValuesCombinatorialRangeCombinatorialRandomData,或實作 ICombinatorialValuesProvider 的自訂屬性。

CombinatorialData 會產生完整的笛卡兒積。 它不支援成對產生、排列、參數值間的排除、成員支持值或類別資料。 當各個值彼此相依,或您需要篩選產生出的測試案例時,請使用 DynamicData 或自訂的 ITestDataSource

DynamicDataAttribute

DynamicDataAttribute 讓你可以提供來自方法、屬性或欄位的測試資料。 當測試資料複雜、動態計算或對內嵌 DataRow 屬性來說過於冗長時,請使用此屬性。

支援的資料來源類型

資料來源可回傳任意 IEnumerable<T> 類型,該 T 類型如下表中列出的。 任何實作 IEnumerable<T> 集合都可運作,包括 List<T>,陣列如 T[],或自訂集合類型。 根據你的需求做出選擇:

傳回類型 型式安全 元資料支援 適用對象
ValueTuple (例如 (int, string)) 編譯時 大多數情境——簡單語法並全型別檢查
Tuple<...> 編譯時 當你無法使用 ValueTuple
TestDataRow<T> 編譯時 Yes 需要顯示名稱、類別或忽略訊息的測試案例
object[] 僅運行時 舊有程式碼 - 避免用於新測試

小提示

對於新的測試資料方法,如果是簡單案例如使用ValueTuple,而需要元資料時則使用TestDataRow<T>。 避免 object[] ,因為它缺乏編譯時型別檢查,且可能因型別不匹配而在執行時出錯。

數據源

資料來源可以是方法或屬性,從 MSTest v3.11 起則是欄位。 這些來源可以互換,請依照你的喜好選擇:

備註

從 MSTest v3.8 開始,會 DynamicData 自動偵測資料來源類型。 你不需要特別說明 DynamicDataSourceType

[TestClass]
public class DynamicDataExample
{
    // Method - best for computed or yielded data
    public static IEnumerable<(int Value, string Name)> GetTestData()
    {
        yield return (1, "first");
        yield return (2, "second");
    }

    // Property - concise for static data
    public static IEnumerable<(int Value, string Name)> TestDataProperty =>
    [
        (1, "first"),
        (2, "second")
    ];

    // Field - simplest for static data
    public static IEnumerable<(int Value, string Name)> TestDataField =
    [
        (1, "first"),
        (2, "second")
    ];

    [TestMethod]
    [DynamicData(nameof(GetTestData))]
    public void TestWithMethod(int value, string name)
    {
        Assert.IsTrue(value > 0);
    }

    [TestMethod]
    [DynamicData(nameof(TestDataProperty))]
    public void TestWithProperty(int value, string name)
    {
        Assert.IsTrue(value > 0);
    }

    [TestMethod]
    [DynamicData(nameof(TestDataField))]
    public void TestWithField(int value, string name)
    {
        Assert.IsTrue(value > 0);
    }
}

備註

資料來源的方法、屬性以及欄位必須是 public static,而且回傳一個支援的型別 IEnumerable<T>

備註

從 MSTest v3.11 開始,資料來源可以是欄位。 早期版本支援方法與屬性。

小提示

相關分析器: MSTEST0018 驗證資料來源的存在、可存取性,以及正確的簽章。

不同類別的數據來源

使用型態參數指定另一個類別:

public class TestDataProvider
{
    public static IEnumerable<(int, string)> GetTestData()
    {
        yield return (1, "first");
        yield return (2, "second");
    }
}

[TestClass]
public class DynamicDataExternalExample
{
    [TestMethod]
    [DynamicData(nameof(TestDataProvider.GetTestData), typeof(TestDataProvider))]
    public void TestMethod(int value1, string value2)
    {
        Assert.IsTrue(value1 > 0);
    }
}

已參數化的 DynamicData 來源方法

從 MSTest v3.10 開始,將值傳給 DynamicData 具有以下 Arguments 特性的來源方法:

[TestMethod]
[DynamicData(nameof(GetValues), Arguments = new object[] { true })]
public void Value_IsPositive(int value, bool expected) => Assert.AreEqual(expected, value > 0);

public static IEnumerable<(int Value, bool Expected)> GetValues(bool includeZero) =>
    includeZero ? [(1, true), (0, false)] : [(1, true)];

自訂顯示名稱

使用 DynamicDataDisplayName 以下屬性自訂測試案例顯示名稱:

using System.Reflection;

[TestClass]
public class DynamicDataDisplayNameExample
{
    [TestMethod]
    [DynamicData(nameof(GetTestData), DynamicDataDisplayName = nameof(GetDisplayName))]
    public void TestMethod(int value1, string value2)
    {
        Assert.IsTrue(value1 > 0);
    }

    public static IEnumerable<(int, string)> GetTestData()
    {
        yield return (1, "first");
        yield return (2, "second");
    }

    public static string GetDisplayName(MethodInfo methodInfo, object[] data)
    {
        return $"{methodInfo.Name} with value {data[0]} and '{data[1]}'";
    }
}

備註

顯示名稱方法必須是 public static,回傳一個 string,並接受兩個參數: MethodInfoobject[]

小提示

若要更簡化自訂顯示名稱,請考慮使用 TestDataRow<T> 及其 DisplayName 屬性,而不是使用單獨的方法。

忽略所有來自資料來源的測試案例

從 MSTest v3.8 開始,使用 IgnoreMessage 來跳過所有測試案例。

[TestClass]
public class IgnoreDynamicDataExample
{
    [TestMethod]
    [DynamicData(nameof(GetTestData), IgnoreMessage = "Feature not ready")]
    public void TestMethod(int value1, string value2)
    {
        // All test cases from GetTestData are skipped
    }

    public static IEnumerable<(int, string)> GetTestData()
    {
        yield return (1, "first");
        yield return (2, "second");
    }
}

小提示

若要忽略個別測試案例,請使用具有 TestDataRow<T> 屬性的 IgnoreMessage。 請參考 TestDataRow<T> 章節。

TestDataRow

TestDataRow<T> 類別在資料驅動測試中提供對測試資料的強化控制。 使用 IEnumerable<T> 作為 TestDataRow<T> 的資料來源回傳類型來指定:

備註

MSTest 於 3.8 版本引入 TestDataRow<T>

  • 自訂顯示名稱:使用DisplayName 屬性為每個測試案例設定唯一的顯示名稱。
  • 測試類別:使用該 TestCategories 屬性將元資料附加到個別測試案例。
  • 忽略訊息:跳過使用該屬性理由 IgnoreMessage 的特定測試案例。
  • 型別安全的資料:使用泛型來建立強型別的測試資料。

基本用法

[TestClass]
public class TestDataRowExample
{
    [TestMethod]
    [DynamicData(nameof(GetTestDataRows))]
    public void TestMethod(int value1, string value2)
    {
        Assert.IsTrue(value1 > 0);
    }

    public static IEnumerable<TestDataRow<(int, string)>> GetTestDataRows()
    {
        yield return new TestDataRow<(int, string)>((1, "first"))
        {
            DisplayName = "Test Case 1: Basic scenario",
        };

        yield return new TestDataRow<(int, string)>((2, "second"))
        {
            DisplayName = "Test Case 2: Edge case",
            TestCategories = ["HighPriority", "Critical"],
        };

        yield return new TestDataRow<(int, string)>((3, "third"))
        {
            IgnoreMessage = "Not yet implemented",
        };
    }
}

DataSourceAttribute

備註

DataSource 僅在 .NET Framework 中提供。 對於 .NET(核心)專案,請使用 DataRowDynamicData 取代。

DataSourceAttribute 能將測試連結到外部資料來源,如 CSV 檔案、XML 檔案或資料庫。

如需詳細資訊,請參閱:

ITestDataSource

介面允許 ITestDataSource 你建立完全自訂的資料來源屬性。 當你需要內建屬性不支援的行為時,例如根據環境變數、設定檔或其他執行時條件產生測試資料,請實作此介面。

介面成員

介面定義了兩種方法:

方法 目標
GetData(MethodInfo) 回傳測試資料為 IEnumerable<object?[]>
GetDisplayName(MethodInfo, object?[]?) 回傳測試案例的顯示名稱

建立自訂資料來源屬性

要建立自訂資料來源,請定義一個屬性類別,此類別應從Attribute繼承並實作ITestDataSource

using System.Globalization;
using System.Reflection;

[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class MyDataSourceAttribute : Attribute, ITestDataSource
{
    public IEnumerable<object?[]> GetData(MethodInfo methodInfo)
    {
        // Return test data based on your custom logic
        yield return [1, "first"];
        yield return [2, "second"];
        yield return [3, "third"];
    }

    public string? GetDisplayName(MethodInfo methodInfo, object?[]? data)
    {
        return data is null
            ? null
            : string.Format(CultureInfo.CurrentCulture, "{0} ({1})", methodInfo.Name, string.Join(",", data));
    }
}

[TestClass]
public class CustomDataSourceExample
{
    [TestMethod]
    [MyDataSource]
    public void TestWithCustomDataSource(int value, string name)
    {
        Assert.IsTrue(value > 0);
        Assert.IsNotNull(name);
    }
}

實務範例:基於環境的測試資料

此範例展示了一個自訂屬性,根據目標框架產生測試資料,並依作業系統進行篩選:

using System.Globalization;
using System.Reflection;

[AttributeUsage(AttributeTargets.Method)]
public class TargetFrameworkDataAttribute : Attribute, ITestDataSource
{
    private readonly string[] _frameworks;

    public TargetFrameworkDataAttribute(params string[] frameworks)
    {
        _frameworks = frameworks;
    }

    public IEnumerable<object?[]> GetData(MethodInfo methodInfo)
    {
        bool isWindows = OperatingSystem.IsWindows();

        foreach (string framework in _frameworks)
        {
            // Skip .NET Framework on non-Windows platforms
            if (!isWindows && framework.StartsWith("net4", StringComparison.Ordinal))
            {
                continue;
            }

            yield return [framework];
        }
    }

    public string? GetDisplayName(MethodInfo methodInfo, object?[]? data)
    {
        return data is null
            ? null
            : string.Format(CultureInfo.CurrentCulture, "{0} ({1})", methodInfo.Name, data[0]);
    }
}

[TestClass]
public class CrossPlatformTests
{
    [TestMethod]
    [TargetFrameworkData("net48", "net8.0", "net9.0")]
    public void TestOnMultipleFrameworks(string targetFramework)
    {
        // Test runs once per applicable framework
        Assert.IsNotNull(targetFramework);
    }
}

小提示

對於簡單的情況,只需要自訂顯示名稱或新增元資料時,可以考慮使用 TestDataRow<T> 來取代實作 DynamicData。 將自訂 ITestDataSource 實作保留給需要動態過濾或複雜資料生成邏輯的情境。

展開策略

資料驅動的測試屬性支援此 TestDataSourceUnfoldingStrategy 屬性,控制測試案例在 Test Explorer 與 TRX 結果中的呈現方式。 這個特性也決定了你是否能獨立執行個別測試案例。

發現與執行階段

MSTest 將資料驅動測試分為兩個明顯階段:

  • 發現階段:MSTest 評估所有資料來源屬性(DataRowDynamicDataITestDataSource),以確定測試案例清單。 此評估 發生在任何 一般 MSTest 生命週期鉤子執行之前——AssemblyInitializeClassInitialize 和其他設定方法尚未執行。
  • 執行階段:MSTest 執行正常的生命週期(組合語言初始化、類別初始化、測試初始化、測試方法、清理),並執行每個測試案例及其資料。

由於資料來源是在發現階段被評估,你的資料產生程式碼無法依賴由 AssemblyInitializeClassInitialize建立的任何狀態。 如果你的資料來源依賴設定邏輯(例如,從初始化的 ClassInitialize資料庫連線讀取),資料來源評估在發現時會失敗。

可用的策略

策略 行為
Auto (預設值) MSTest 決定最佳的展開策略。
Unfold 所有測試案例均會擴充並個別展示。
Fold 所有測試用例都整合在單一測試節點中。

摺疊測試與展開測試

展開策略會影響測試結果在 Test Explorer 與 TRX 輸出中的呈現方式。

  • 展開測試:每個資料列在 Test Explorer 和 TRX 中都以獨立的測試項目出現。 你可以執行、除錯或過濾個別測試案例。 每個項目都有自己的通過或不通過狀態。
  • 摺疊測試:所有資料列在測試檔案總管中都顯示為單一測試節點。 在 TRX 中出現了一個條目,該單個測試案例與多個結果相關聯。 你無法獨立執行或過濾單一資料列。

探索過程中的例外會導致折疊

當 MSTest 在發現過程中評估資料來源且拋出例外時,無論如何配置展開策略,測試都會退回到摺疊狀態。 由於框架無法列舉個別測試案例,因此將測試方法註冊為單一(摺疊)條目。

執行時,MSTest 會依照正常生命週期再次評估資料來源。 如果例外是因為缺失的設置狀態(例如 ClassInitialize 提供的依賴性),資料來源在執行時可能會成功,因為生命週期掛鉤已經運行過。

備註

在除錯一個在發現過程中拋出的測試時,你可能會在測試開始前看到一個例外(例如 a NullReferenceException)。 此例外來自發現階段評估。 在除錯器中按下 繼續 ——測試會正常執行,因為執行階段會執行完整的 MSTest 生命週期,包括初始化方法。 更多細節請參見 microsoft/testfx#7774

何時改變策略

在大多數情況下,預設 Auto 行為提供了最佳的平衡。 當您有具體需求時,考慮改變展開策略:

  • 如果你的資料來源依賴於執行時狀態或設定邏輯,而這些邏輯在發現時無法取得,就使用 Fold
  • 使用 Fold 來處理非確定性資料來源,它們在每次評估時會回傳不同的值。
  • 當測試案例數量眾多,若對效能有所顧慮,可用 Fold 來降低開銷。

範例使用方式

[TestClass]
public class UnfoldingExample
{
    [TestMethod(UnfoldingStrategy = TestDataSourceUnfoldingStrategy.Unfold)] // That's the default behavior
    [DataRow(1, "one")]
    [DataRow(2, "two")]
    [DataRow(3, "three")]
    public void TestMethodWithUnfolding(int value, string text)
    {
        // Each test case appears individually in Test Explorer
    }

    [TestMethod(UnfoldingStrategy = TestDataSourceUnfoldingStrategy.Fold)]
    [DataRow(1, "one")]
    [DataRow(2, "two")]
    [DataRow(3, "three")]
    public void TestMethodWithFolding(int value, string text)
    {
        // All test cases appear as a single collapsed node
    }
}

最佳做法

  • 選擇正確的屬性:簡單的內嵌資料使用 DataRow,獨立輸入的所有可能組合使用 CombinatorialData,複雜或經過運算的資料則使用 DynamicData
  • 命名你的測試案例:使用 DisplayName 來讓測試失敗更容易辨識。
  • 保持資料來源近在咫尺:盡可能將資料來源定義為同一類別,以提升維護性。
  • 使用有意義的數據:選擇能模擬邊緣案例與邊界條件的測試數據。

另請參閱