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 支持各种参数类型,包括基元、字符串、数组和 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<(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);
    }
}

自定义显示名称

使用 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> 作为数据源返回类型,以指定:

  • 自定义显示名称:使用属性 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(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 结果中的显示方式。 此属性还确定是否可以独立运行单个测试用例。

发现和执行阶段

MSTest 在两个不同的阶段处理数据驱动测试:

  • 发现阶段:MSTest 评估所有数据源属性(DataRow,, DynamicDataITestDataSource以确定测试用例的列表。 此评估在任何常规 MSTest 生命周期挂钩运行之前进行 - AssemblyInitializeClassInitialize 和其他设置方法尚未执行。
  • 执行阶段:MSTest 运行正常生命周期(程序集初始化、类初始化、测试初始化、测试方法、清理),并使用其数据执行每个测试用例。

由于数据源是在发现期间评估的,因此数据生成代码不能依赖于由 AssemblyInitializeClassInitialize设置的任何状态。 如果数据源依赖于设置逻辑(例如,从初始化的数据库 ClassInitialize连接进行读取),则数据源评估在发现过程中会失败。

可用策略

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

折叠的测试与展开的测试

展开的策略会影响测试资源管理器和 TRX 输出中报告测试结果的方式:

  • 展开的测试:在测试资源管理器和 TRX 中,每个数据行都显示为一个独立的测试条目。 可以运行、调试或筛选单个测试用例。 每个条目都有其自己的通过/失败状态。
  • 折叠测试:所有数据行在测试资源管理器中显示为单个测试节点。 TRX 中会出现一个条目,其中包含与该单个测试用例关联的多个结果。 不能单独运行或筛选单个数据行。

发现期间出现异常会导致折叠

当 MSTest 在发现期间评估数据源并引发异常时,无论配置的展开策略如何,测试都会回退到折叠状态。 由于框架无法枚举单个测试用例,因此它将测试方法注册为单个(折叠)条目。

在执行时,MSTest 会在正常生命周期中再次评估数据源。 如果异常是由于缺少配置状态(例如 ClassInitialize 提供的依赖项)引起的,则数据源可能会在执行期间成功,因为生命周期挂钩已经被调用。

注释

当调试一个在发现期间抛出异常的测试时,你可能会在测试开始之前看到异常(例如,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 包。

另请参阅