创建数据驱动的单元测试

可以使用 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 属性来指定将提供数据驱动测试所用数据的成员的名称、种类(属性、默认值或方法)和定义类型(默认情况下使用当前类型)。

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 的测试方法很简单。 针对数据源中的每一行,将 FirstNumber 和 SecondNumber 列值作为参数来调用 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 构造函数指定数据源的连接字符串和包含测试方法的数据的表的名称。

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

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

使用 TestContext.DataRow 访问数据

若要访问 AddIntegersData 表中的数据,请使用 TestContext.DataRow 索引器。 DataRowDataRow 对象,因此根据索引或列名称检索列的值。 由于值作为对象返回,需将它们转换为合适的类型:

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

运行测试并查看结果

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

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

注意

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

如果在我们的示例中运行 AddIntegers_FromDataRowTestAddIntegers_FromDynamicDataTestAddIntegers_FromDataSourceTest 方法中的任何一个,则结果栏变为红色,且测试方法移动到“失败的测试”。 如果数据源中任一循环访问的方法失败,则数据驱动测试失败。 在测试资源管理器窗口中选择失败的数据驱动测试时,细节窗格将显示由数据行索引标识的每个迭代的结果。 在本示例中 AddIntegers 算法好像没有正确处理负值。

当更正了要测试的方法,并重新运行测试,则结果栏变为绿色,并且测试方法移动到“通过的测试”组中。