我正在组织一个关于单元测试的好处的演示文稿,我想要一个意想不到的后果的简单示例:在一个类中更改代码以破坏另一个类中的功能。
有人可以建议一个简单,易于解释的例子吗?
我的计划是围绕此功能编写单元测试,以证明我们知道我们通过立即运行测试来破坏了某些东西。
答案 0 :(得分:12)
一个稍微简单,因此可能更清晰的例子是:
public string GetServerAddress()
{
return "172.0.0.1";
}
public void DoSomethingWithServer()
{
Console.WriteLine("Server address is: " + GetServerAddress());
}
如果GetServerAddress
更改为返回数组:
public string[] GetServerAddress()
{
return new string[] { "127.0.0.1", "localhost" };
}
DoSomethingWithServer的输出会有所不同,但它仍然会编译,从而产生更微妙的错误。
第一个(非阵列)版本将打印Server address is: 127.0.0.1
,第二个将打印Server address is: System.String[]
,这也是我在生产代码中看到的。毋庸置疑,它不再存在!
答案 1 :(得分:8)
以下是一个例子:
class DataProvider {
public static IEnumerable<Something> GetData() {
return new Something[] { ... };
}
}
class Consumer {
void DoSomething() {
Something[] data = (Something[])DataProvider.GetData();
}
}
更改GetData()
以返回List<Something>
,Consumer
将会中断。
这看起来有点人为,但我在实际代码中看到了类似的问题。
答案 2 :(得分:4)
假设你有一个方法:
abstract class ProviderBase<T>
{
public IEnumerable<T> Results
{
get
{
List<T> list = new List<T>();
using(IDataReader rdr = GetReader())
while(rdr.Read())
list.Add(Build(rdr));
return list;
}
}
protected abstract IDataReader GetReader();
protected T Build(IDataReader rdr);
}
使用各种实现。其中一个用于:
public bool CheckNames(NameProvider source)
{
IEnumerable<string> names = source.Results;
switch(names.Count())
{
case 0:
return true;//obviously none invalid.
case 1:
//having one name to check is a common case and for some reason
//allows us some optimal approach compared to checking many.
return FastCheck(names.Single());
default:
return NormalCheck(names)
}
}
现在,这一切都不是特别奇怪。我们没有假设IEnumerable的特定实现。实际上,这将适用于数组和许多常用的集合(不能想到System.Collections.Generic中的一个与我的头顶不匹配)。我们只使用了常规方法和常规扩展方法。对于单项集合而言,优化案例并不罕见。例如,我们可以将列表更改为数组,或者可能是HashSet(自动删除重复项),或者是LinkedList或其他一些内容,它会继续工作。
尽管我们并不依赖于特定的实现,但我们依赖于一个特定的功能,特别是可重绕的功能(Count()
将调用ICollection.Count或枚举通过枚举,之后将进行名称检查。
有人虽然看到了结果属性并认为“嗯,这有点浪费”。他们将其替换为:
public IEnumerable<T> Results
{
get
{
using(IDataReader rdr = GetReader())
while(rdr.Read())
yield return Build(rdr);
}
}
这又是完全合理的,并且在许多情况下确实会带来相当大的性能提升。如果CheckNames
未被相关编码器立即执行的“测试”命中(可能没有在很多代码路径中命中),那么CheckNames将会出错(并且可能返回false)导致超过1个名称的情况,如果它打开安全风险可能会更糟糕。)
任何以超过零结果击中CheckNames的单元测试都会抓住它。
顺便提一下,类似(如果更复杂)的更改是NPGSQL中向后兼容功能的原因。只是用返回yield替换List.Add()并不简单,但对ExecuteReader工作方式的改变给出了从O(n)到O(1)的可比较的变化以获得第一个结果。但是,在此之前,NpgsqlConnection允许用户从第一个仍处于打开状态的连接中获取另一个读取器,之后不会。 IDbConnection的文档说你不应该这样做,但这并不意味着没有运行代码。幸运的是,一个这样的运行代码是NUnit测试,并且添加了向后兼容性功能,允许这些代码只需更改配置即可继续运行。