考虑以下课程:
public class MyIntSet
{
private List<int> _list = new List<int>();
public void Add(int num)
{
if (!_list.Contains(num))
_list.Add(num);
}
public bool Contains(int num)
{
return _list.Contains(num);
}
}
遵循“仅测试一件事”原则,假设我想测试“添加”功能。 考虑以下这种测试的可能性:
[TestClass]
public class MyIntSetTests
{
[TestMethod]
public void Add_AddOneNumber_SetContainsAddedNumber()
{
MyIntSet set = new MyIntSet();
int num = 0;
set.Add(num);
Assert.IsTrue(set.Contains(num));
}
}
我对这个解决方案的问题是它实际上测试了 2个方法:Add()和Contains()。 从理论上讲,两者都可能存在错误,只会出现在一个接一个不被调用的情况下。当然,Contains()现在服务器作为List的Contains()的瘦包装器,它本身不应该进行测试,但如果它将来会变得更复杂呢?也许应该始终保留一个简单的“薄包裹”方法用于测试目的?
另一种方法可能会建议模拟或公开(可能使用InternalsVisibleTo或PrivateObject)私有_list成员并让测试直接检查它,但如果有一天内部列表被其他一些集合替换,则可能会产生测试可维护性问题(也许是C5)。
有更好的方法吗? 我反对上述实现的任何论据都有缺陷吗?
提前致谢, JC
答案 0 :(得分:9)
你的测试对我来说似乎完全没问题。您可能误解了单元测试的原则。
单个测试应该(理想情况下)只测试一件事,这是事实,但这并不意味着它应该只测试一种方法;相反,它应该只测试一个行为(一个不变量,遵守某个业务规则等)。
您的测试测试行为“如果您添加到新集合,它不再为空”,这是一种行为: - )。
解决您的其他问题:
从理论上讲,两者都可能存在错误,只会出现在一个接一个调用它们的情况下。
没错,但这只是意味着你需要更多测试:-)。例如,添加两个数字,然后调用Contains,或调用Contains而不添加。
另一种方法可能会建议模拟或公开(可能使用InternalsVisibleTo)私有_list成员并让测试直接检查它,但这可能会产生测试可维护性问题[...]
非常,所以不要这样做。单元测试应始终针对被测设备的公共接口。这就是为什么它被称为单元测试,而不是“在一个单元内乱搞”-test; - )。
答案 1 :(得分:1)
有两种可能性。
你暴露了设计中的一个缺陷。您应该仔细考虑您的Add方法正在执行的操作对于使用者是否清楚。如果您不希望人们向列表中添加重复项,为什么还要使用Contains()方法呢?当用户没有添加到列表中并且没有抛出错误时,将会感到困惑。更糟糕的是,他们可能会在他们的列表集合上调用.Add()之前编写完全相同的代码来复制功能。也许它应该删除,并用索引器代替?从列表类中不清楚它并不意味着重复。
设计很好,您的公共方法应该相互依赖。这是正常的,没有理由你不能测试这两种方法。你拥有的测试案例越多,理论上就越好。
作为一个例子,假设你有一个只调用其他层的函数,这些层可能已经过单元测试。这并不意味着你不会为函数编写单元测试,即使它只是一个包装器。
答案 2 :(得分:1)
在实践中,您当前的测试很好。对于这个简单的事情,add()和contains()中的错误不太可能相互串谋隐藏对方。如果您真的担心单独测试add()和add(),一种解决方案是让您的_list变量可用于您的单元测试代码。
[TestClass]
public void Add_AddOneNumber_SetContainsAddedNumber() {
MyIntSet set = new MyIntSet();
set.add(0);
Assert.IsTrue(set._list.Contains(0));
}
这样做有两个缺点。一:它需要访问私有_list变量,a little complex in C#(我推荐the reflection technique)。二:它使您的测试代码依赖于Set实现的实际实现,这意味着如果您更改了实现,则必须修改测试。我从来没有像集合类那样简单,但在某些情况下它可能很有用。