通过


MSTest 中的数据驱动测试

通过数据驱动测试,可以使用多个输入数据集运行相同的测试方法。 不必为每个测试用例编写单独的测试方法,只需定义一次测试逻辑,并通过属性或外部数据源提供不同的输入。

概述

MSTest 为数据驱动测试提供了多个属性:

Attribute 用例 最适用于
DataRow 内联测试数据 简单静态测试用例
DynamicData 来自方法、属性或字段的数据 复杂或计算的测试数据
TestDataRow<T> 使用元数据增强的数据 需要显示名称或类别的测试用例
DataSource 外部数据文件或数据库 外部数据源的遗留方案
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 支持各种参数类型,包括基元、字符串、数组和 null 值:

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

对变量参数使用参数

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

自定义显示名称

设置属性 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>DynamicData. TestDataRow<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> 编译时 是的 需要显示名称、类别或忽略消息的测试用例
object[] 仅运行时 遗留代码 - 避免在新测试中使用

小窍门

对于新的测试数据方法,在简单情况下请使用 ValueTuple,在需要元数据时请使用 TestDataRow<T>。 避免 object[] 因为它缺少编译时类型检查,并可能导致类型不匹配的运行时错误。

数据源

数据源可以是方法、属性或字段。 这三者都是可互换的- 根据偏好进行选择:

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

小窍门

相关分析器: MSTEST0018 验证数据源是否存在、可访问且具有正确的签名。

来自不同类的数据源

使用类型参数指定其他类:

public class TestDataProvider
{
    public static IEnumerable<object[]> GetTestData()
    {
        yield return new object[] { 1, "first" };
        yield return new object[] { 2, "second" };
    }
}

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

自定义显示名称

使用 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<object[]> GetTestData()
    {
        yield return new object[] { 1, "first" };
        yield return new object[] { 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<object[]> GetTestData()
    {
        yield return new object[] { 1, "first" };
        yield return new object[] { 2, "second" };
    }
}

小窍门

若要忽略单个测试用例,请使用 TestDataRow<T>IgnoreMessage 属性。 请参阅 TestDataRow<T> 部分。

TestDataRow

TestDataRow<T> 类提供对数据驱动测试中测试数据的增强控制。 使用 IEnumerable<TestDataRow<T>> 作为数据源的返回类型进行指定:

  • 自定义显示名称:为每个测试用例设置唯一的显示名称
  • 测试类别:将元数据附加到单个测试用例
  • 忽略消息:跳过具有原因的特定测试用例
  • 类型安全数据:对强类型测试数据使用泛型

基本用法

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

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

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

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

DataSourceAttribute

注释

DataSource 仅在 .NET Framework 中可用。 对于 .NET(Core)项目,请使用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。 为需要动态筛选或复杂数据生成逻辑的方案保留自定义 ITestDataSource 实现。

展开策略

数据驱动测试属性支持该 TestDataSourceUnfoldingStrategy 属性,该属性控制测试用例在测试资源管理器和 TRX 结果中的显示方式。 此属性还确定是否可以独立运行单个测试用例。

可用策略

策略 行为
Auto(默认值) MSTest 确定最佳展开策略
Unfold 所有测试用例都会展开并单独显示
Fold 所有测试用例都折叠成单个测试节点

何时更改策略

对于大多数方案,默认 Auto 行为提供最佳平衡。 仅当有特定要求时,才考虑更改此设置:

  • 非确定性数据源
  • MSTest 中的已知限制性或错误
  • 许多测试用例的性能问题

示例用法

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

    [TestMethod(UnfoldingStrategy = TestDataSourceUnfoldingStrategy.Fold)]
    [DynamicData(nameof(GetData))]
    public void TestMethodWithFolding(int value, string text)
    {
        // All test cases appear as a single collapsed node
    }

    public static IEnumerable<(int, string)> GetData()
    {
        yield return (1, "one");
        yield return (2, "two");
        yield return (3, "three");
    }
}

最佳做法

  1. 选择正确的属性:使用 DataRow 来处理简单的内联数据。 使用 DynamicData 来处理复杂或计算数据。

  2. 命名测试用例:利用 DisplayName 简化识别测试失败的过程。

  3. 使数据源保持关闭:尽可能在同一类中定义数据源以提高可维护性。

  4. 使用有意义的数据:选择练习边缘事例和边界条件的测试数据。

  5. 考虑组合测试:对于测试参数组合,请使用 Combinatorial.MSTest 包。

另请参阅