单元测试 - 测试用例与多种方法
Unit Testing - Tests Cases vs. Multiple Methods
在为下面的示例编写单元测试时,最好的做法是使用 TestCases(例如在 NUnit 中),还是编写多个测试来验证功能?
假设您要测试下面的 RetrieveContact 方法 - 它只是在数组中找到相应的联系人,如果找到则 return 返回结果,否则 return 为空。
public class Contact
{
public string Name { get; set; }
}
private static Contact[] Contacts =
{
new Contact() { Name = "Jim" },
new Contact() { Name = "Bob" },
new Contact() { Name = "Tom" }
};
public static Contact RetrieveContact(string name)
{
return Contacts.FirstOrDefault(c => c.Name == name);
}
你会用一种方法测试这个吗,使用 TestCases,如下所示?
[TestCase("Bob", "Bob")]
[TestCase(null, "ZZZ")]
public void Test_RetrievesFromContacts(string expectedName, string name)
{
var ret = RetrieveContact(name);
Assert.AreEqual(expectedName, ret?.Name);
}
或者您会编写两个单独的测试 - 一个用于有效联系人,一个用于无效联系人 returning null?
[Test]
public void Test_RetrieveValidContact()
{
var ret = RetrieveContact("Bob");
Assert.AreEqual("Bob", ret.Name);
}
[Test]
public void Test_RetrieveInvalidContact()
{
var ret = RetrieveContact("ZZZ");
Assert.AreEqual(null, ret);
}
谢谢
JUnit 中的约定是每个 unit 测试都应该测试一个特定的功能,一个通过被测代码的不同路径。此约定强烈暗示您不应将多个不相关的测试合并到一个测试方法中。
因此,在上面的示例中,您将为以下各项编写一个测试:
- 有效联系人一个
- 一个用于无效联系人
这样做的理由是:
- 如果将上述两条测试路径组合成一个测试,那么第一条路径中的失败可能会阻止第二条路径运行(从而降低测试覆盖率)
- 在所有其他条件相同的情况下,细粒度的测试用例可能更简洁易读,即不太可能受到
if firstPathFails then log and move on to second path
等条件性的影响
- 如果您对 "invalid contacts" 的处理发生变化,则不必更改解决快乐路径的测试用例(在您的情况下为 "valid contacts")。这是对 SRP.
的引用
备注:
- 如果测试用例中的多个相关方法需要共享设置代码,则可以在
@Before
方法中实现,从而减少每个 @Test
方法的 size/reposibilities。
- 上述约定并不意味着单个测试方法只能有一个断言。
- 上述约定不一定适用于粗粒度测试,例如集成测试,其中可能会测试应用程序的整个部分。
尽管您用 junit、xunit 和 nunit 标记了 post,但代码显然是 NUnit。已经 posted 的面向 junit 的答案对于 junit 无疑是正确的,但我将重点关注您正在使用的框架和语言。
你给出的两个例子在NUnit中是完全等价的。在参数化方法中,每个测试用例作为独立于另一个的单独测试运行。每个结果单独报告。
NUnit 的 TestCaseAttribute
只不过是创建多个测试的语法快捷方式,它们仅在使用的值上有所不同。您使用哪种方法是风格和命名偏好的问题。 TestCase
方法的使用在我看来更能吸引可能决定添加新数据点的其他程序员。
如果在某些情况下您要使用的数据类型在 C# 中不允许作为属性的构造函数参数,您还应该考虑使用 TestCaseSourceAttribute
。
我对您的特定用例持保留意见。当 "failure" 案例只是 returns null 且没有其他副作用时,它工作正常。然而,在大多数真实场景中,失败案例会抛出异常、生成错误报告等。一般来说,对于一个或多个失败案例,通常最好使用一个完全独立的方法。
在为下面的示例编写单元测试时,最好的做法是使用 TestCases(例如在 NUnit 中),还是编写多个测试来验证功能?
假设您要测试下面的 RetrieveContact 方法 - 它只是在数组中找到相应的联系人,如果找到则 return 返回结果,否则 return 为空。
public class Contact
{
public string Name { get; set; }
}
private static Contact[] Contacts =
{
new Contact() { Name = "Jim" },
new Contact() { Name = "Bob" },
new Contact() { Name = "Tom" }
};
public static Contact RetrieveContact(string name)
{
return Contacts.FirstOrDefault(c => c.Name == name);
}
你会用一种方法测试这个吗,使用 TestCases,如下所示?
[TestCase("Bob", "Bob")]
[TestCase(null, "ZZZ")]
public void Test_RetrievesFromContacts(string expectedName, string name)
{
var ret = RetrieveContact(name);
Assert.AreEqual(expectedName, ret?.Name);
}
或者您会编写两个单独的测试 - 一个用于有效联系人,一个用于无效联系人 returning null?
[Test]
public void Test_RetrieveValidContact()
{
var ret = RetrieveContact("Bob");
Assert.AreEqual("Bob", ret.Name);
}
[Test]
public void Test_RetrieveInvalidContact()
{
var ret = RetrieveContact("ZZZ");
Assert.AreEqual(null, ret);
}
谢谢
JUnit 中的约定是每个 unit 测试都应该测试一个特定的功能,一个通过被测代码的不同路径。此约定强烈暗示您不应将多个不相关的测试合并到一个测试方法中。
因此,在上面的示例中,您将为以下各项编写一个测试:
- 有效联系人一个
- 一个用于无效联系人
这样做的理由是:
- 如果将上述两条测试路径组合成一个测试,那么第一条路径中的失败可能会阻止第二条路径运行(从而降低测试覆盖率)
- 在所有其他条件相同的情况下,细粒度的测试用例可能更简洁易读,即不太可能受到
if firstPathFails then log and move on to second path
等条件性的影响 - 如果您对 "invalid contacts" 的处理发生变化,则不必更改解决快乐路径的测试用例(在您的情况下为 "valid contacts")。这是对 SRP. 的引用
备注:
- 如果测试用例中的多个相关方法需要共享设置代码,则可以在
@Before
方法中实现,从而减少每个@Test
方法的 size/reposibilities。 - 上述约定并不意味着单个测试方法只能有一个断言。
- 上述约定不一定适用于粗粒度测试,例如集成测试,其中可能会测试应用程序的整个部分。
尽管您用 junit、xunit 和 nunit 标记了 post,但代码显然是 NUnit。已经 posted 的面向 junit 的答案对于 junit 无疑是正确的,但我将重点关注您正在使用的框架和语言。
你给出的两个例子在NUnit中是完全等价的。在参数化方法中,每个测试用例作为独立于另一个的单独测试运行。每个结果单独报告。
NUnit 的 TestCaseAttribute
只不过是创建多个测试的语法快捷方式,它们仅在使用的值上有所不同。您使用哪种方法是风格和命名偏好的问题。 TestCase
方法的使用在我看来更能吸引可能决定添加新数据点的其他程序员。
如果在某些情况下您要使用的数据类型在 C# 中不允许作为属性的构造函数参数,您还应该考虑使用 TestCaseSourceAttribute
。
我对您的特定用例持保留意见。当 "failure" 案例只是 returns null 且没有其他副作用时,它工作正常。然而,在大多数真实场景中,失败案例会抛出异常、生成错误报告等。一般来说,对于一个或多个失败案例,通常最好使用一个完全独立的方法。