创建数据驱动单元测试

可以使用托管代码的 Microsoft 单元测试框架 (MSTest) 设置单元测试方法以从数据源检索值。 该方法针对数据源中的每个行连续运行,这样就可以使用单个方法轻松测试各种输入。

数据驱动单元测试可以使用以下任一类型:

  • 使用 DataRow 属性的内联数据
  • 使用 DynamicData 属性的成员数据
  • 来自一些使用 DataSource 属性的已知源提供程序

正在测试的方法

例如,假设你有:

  1. 名为 MyBank 的解决方案,可接受和处理不同类型的帐户的事务。

  2. MyBank 中名为 BankDb 的项目,用于管理帐户的事务。

  3. BankDb 项目中名为 Maths 的类,用于执行数学函数以确保任何事务对银行有利。

  4. 名为 BankDbTests 的单元测试项目,用于测试 BankDb 组件的行为。

  5. 名为 MathsTests 的单元测试类,用于验证 Maths 类的行为。

我们将在 Maths 中测试使用循环添加两个整数的方法:

public int AddIntegers(int first, int second)
{
    int sum = first;
    for (int i = 0; i < second; i++)
    {
        sum += 1;
    }

    return sum;
}

对测试方法进行测试

内联数据驱动的测试

对于内联测试,MSTest 使用 DataRow 来指定数据驱动测试使用的值。 此示例中的测试针对每个数据行连续运行。

[TestMethod]
[DataRow(1, 1, 2)]
[DataRow(2, 2, 4)]
[DataRow(3, 3, 6)]
[DataRow(0, 0, 1)] // The test run with this row fails
public void AddIntegers_FromDataRowTest(int x, int y, int expected)
{
    var target = new Maths();
    int actual = target.AddIntegers(x, y);
    Assert.AreEqual(expected, actual,
        "x:<{0}> y:<{1}>",
        new object[] {x, y});
}

成员数据驱动的测试

MSTest 使用 DynamicData 属性来指定成员的名称、类型和定义类型(默认使用当前类型),该成员将提供数据驱动测试使用的数据。

注释

在 MSTest 3.8 之前,DynamicDataSourceType 枚举具有两个成员,PropertyMethod。 默认值为 Property。 从 MSTest 3.8 开始,新成员 AutoDetect 添加到枚举中,这是默认值。 因此,不再需要指定 DynamicDataSourceType

public static IEnumerable<object[]> AdditionData
{
    get
    {
        return new[]
        { 
            new object[] { 1, 1, 2 },
            new object[] { 2, 2, 4 },
            new object[] { 3, 3, 6 },
            new object[] { 0, 0, 1 }, // The test run with this row fails
        };
    }
}

[TestMethod]
[DynamicData(nameof(AdditionData))]
public void AddIntegers_FromDynamicDataTest(int x, int y, int expected)
{
    var target = new Maths();
    int actual = target.AddIntegers(x, y);
    Assert.AreEqual(expected, actual,
        "x:<{0}> y:<{1}>",
        new object[] {x, y});
}

还可以使用 DynamicData 属性的 DynamicDataDisplayName 属性替代默认生成的显示名称。 显示名称方法签名必须 public static string 并接受两个参数,第一个类型 MethodInfo 和第二个类型 object[]

public static string GetCustomDynamicDataDisplayName(MethodInfo methodInfo, object[] data)
{
    return string.Format("DynamicDataTestMethod {0} with {1} parameters", methodInfo.Name, data.Length);
}

[DynamicData(nameof(AdditionData), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))]

源提供程序数据驱动的测试

创建数据源驱动的单元测试涉及以下步骤:

  1. 创建一个数据源,其中包含在测试方法中使用的值。 数据源可以是任何在运行测试的计算机上注册的类型。

  2. TestContext 类型的公共 TestContext 属性添加到测试类。

  3. 创建单元测试方法

  4. 向其添加 DataSourceAttribute 属性。

  5. 使用 DataRow 索引器属性检索在测试中使用的值。

创建数据源

若要测试 AddIntegers 方法,请创建一个数据源,该数据源指定参数的一系列值以及预期返回的总和。 在本示例中,我们将创建名为 MathsData 的 Sql Compact 数据库和包含以下列名和值的名为 AddIntegersData 的表

FirstNumber SecondNumber Sum
0 1 1
1 1 2
2 -3 -1

将 TestContext 添加到测试类

单元测试框架创建一个 TestContext 对象,用于存储数据驱动测试的数据源信息。 然后,框架将此对象设置为所创建的 TestContext 属性的值。

public TestContext TestContext { get; set; }

在您的测试方法中,您可以通过 TestContextDataRow 索引器属性访问数据。

注释

.NET Core 不支持 DataSource 属性。 如果尝试在.NET Core、UWP 或 WinUI 单元测试项目中以这种方式访问测试数据,您可能会看到与 "‘TestContext’ 不包含 'DataRow' 的定义,且找不到接受 'TestContext' 类型的第一个参数的可访问扩展方法 'DataRow'" 类似的错误(是否缺少 using 指令或程序集引用?)

编写测试方法

AddIntegers 的测试方法非常简单。 对于数据源中的每个行,请使用 FirstNumberSecondNumber 列值作为参数调用 AddIntegers,并针对 Sum 列值验证返回值:

[TestMethod]
[DataSource(@"Provider=Microsoft.SqlServerCe.Client.4.0; Data Source=C:\Data\MathsData.sdf;", "Numbers")]
public void AddIntegers_FromDataSourceTest()
{
    var target = new Maths();

    // Access the data
    int x = Convert.ToInt32(TestContext.DataRow["FirstNumber"]);
    int y = Convert.ToInt32(TestContext.DataRow["SecondNumber"]);
    int expected = Convert.ToInt32(TestContext.DataRow["Sum"]);
    int actual = target.AddIntegers(x, y);
    Assert.AreEqual(expected, actual,
        "x:<{0}> y:<{1}>",
        new object[] {x, y});
}

指定 DataSourceAttribute

DataSource 属性指定数据源的连接字符串以及测试方法中使用的表的名称。 连接字符串中的确切信息会有所不同,具体取决于所使用的数据源类型。 在此示例中,我们使用了 SqlServerCe 数据库。

[DataSource(@"Provider=Microsoft.SqlServerCe.Client.4.0;Data Source=C:\Data\MathsData.sdf", "AddIntegersData")]

注意

连接字符串可以包含敏感数据(例如密码)。 连接字符串以纯文本形式存储在源代码和已编译的程序集中。 限制对源代码和程序集的访问以保护此敏感信息。

DataSource 属性有三个构造函数。

[DataSource(dataSourceSettingName)]

具有一个参数的构造函数使用存储在解决方案 app.config 文件中的连接信息。 dataSourceSettingsName 是配置文件中指定连接信息的 Xml 元素的名称。

使用 app.config 文件可以更改数据源的位置,而无需更改单元测试本身。 有关如何创建和使用 app.config 文件的信息,请参阅 演练:使用配置文件定义数据源

[DataSource(connectionString, tableName)]

具有两个参数的 DataSource 构造函数指定数据源的连接字符串和包含测试方法数据的表的名称。

连接字符串取决于数据源类型的类型,但它应包含一个提供程序元素,该元素指定数据提供程序的固定名称。

[DataSource(
    dataProvider,
    connectionString,
    tableName,
    dataAccessMethod
    )]

使用 TestContext.DataRow 访问数据

若要访问 AddIntegersData 表中的数据,请使用 TestContext.DataRow 索引器。 DataRow 是一个 DataRow 对象,因此按索引或列名检索列值。 由于这些值作为对象返回,因此将它们转换为适当的类型:

int x = Convert.ToInt32(TestContext.DataRow["FirstNumber"]);

运行测试和查看结果

编写完测试方法后,生成测试项目。 测试方法显示在 测试资源管理器未运行测试 组中。 运行、写入和重新运行测试时,测试资源管理器失败的测试组、通过的测试以及 未运行测试显示结果。 可以选择 运行所有 来运行所有测试,或选择 运行 选择要运行的测试子集。

当运行测试时,位于“测试资源管理器”顶部的测试结果栏是动态显示的。 在测试运行结束时,如果所有测试都已通过,则条形图将为绿色,如果任何测试均失败,则为红色。 测试运行摘要显示在 测试资源管理器 窗口底部的详细信息窗格中。 选择一个测试,在底部窗格中查看该测试的详细信息。

注释

每行数据都有一个结果,还有一个摘要结果。 如果每行数据测试均通过,则摘要运行显示为“已通过”。 如果测试在任何数据行上失败,摘要运行将显示为 失败

如果在示例中运行了任何 AddIntegers_FromDataRowTestAddIntegers_FromDynamicDataTestAddIntegers_FromDataSourceTest 方法,结果栏将变为红色,并且测试方法将移动到 失败的测试区。 如果数据源中的任何迭代方法失败,数据驱动测试将失败。 在 测试资源管理器 窗口中选择失败的数据驱动测试时,详细信息窗格将显示数据行索引标识的每个迭代的结果。 在我们的示例中,AddIntegers 算法似乎未正确处理负值。

当测试中的方法得到更正并重新运行测试时,结果栏变为绿色,并将测试方法移动到 通过的测试 组。