MSTest 中的數據驅動測試

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

概觀

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

Attribute 用例 適用對象
DataRow 線上測試資料 簡單、靜態的測試案例
DynamicData 來自方法、屬性或欄位的資料 複雜或計算測試資料
DataSource 外部資料檔案或資料庫 帶有外部資料來源的舊有情境

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

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

小提示

對於組合測試(測試所有多個參數集合的組合),請使用開源的 Combinatorial.MSTest NuGet 套件。 這個由社群維護的套件可以在 GitHub 上取得 ,但並非由 Microsoft 維護。

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
    }
}

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來自簡的內聯資料。 將 DynamicData 用於處理複雜或計算的資料。
  • 命名你的測試案例:使用 DisplayName 來讓測試失敗更容易辨識。
  • 保持資料來源近在咫尺:盡可能將資料來源定義為同一類別,以提升維護性。
  • 使用有意義的數據:選擇能模擬邊緣案例與邊界條件的測試數據。
  • 考慮組合測試:測試參數組合時,使用 Combinatorial.MSTest 套件。

另請參閱