8

我正在整理一个关于单元测试的好处的演示文稿,我想要一个意外后果的简单示例:更改一个类中的代码会破坏另一个类中的功能。

有人可以提出一个简单易懂的例子吗?

我的计划是围绕这个功能编写单元测试,以证明我们知道通过立即运行测试我们破坏了某些东西。

4

3 回答 3

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[],这也是我在生产代码中看到的。不用说它不再存在了!

于 2010-08-12T22:02:08.893 回答
8

这是一个例子:

class DataProvider {
    public static IEnumerable<Something> GetData() {
        return new Something[] { ... };
    }
}

class Consumer {
    void DoSomething() {
        Something[] data = (Something[])DataProvider.GetData();
    }
}

更改GetData()为返回 a List<Something>Consumer并将中断。

这可能看起来有些做作,但我在实际代码中看到了类似的问题。

于 2010-08-12T21:53:50.047 回答
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 或通过枚举进行枚举,之后将进行名称检查。

有人虽然看到 Results 属性并认为“嗯,这有点浪费”。他们将其替换为:

public IEnumerable<T> Results
{
  get
  {
    using(IDataReader rdr = GetReader())
      while(rdr.Read())
        yield return Build(rdr);
  }
}

这又是完全合理的,并且在许多情况下确实会带来相当大的性能提升。如果CheckNames在有问题的编码器完成的即时“测试”中没有命中(可能在很多代码路径中没有命中),那么 CheckNames 将出错(并且可能在以下情况下返回错误结果)超过 1 个名称,如果它会带来安全风险,可能会更糟)。

Any unit test that hits on CheckNames with the more than zero results is going to catch it though.


Incidentally a comparable (if more complicated) change is a reason for a backwards-compatibility feature in NPGSQL. Not quite as simple as just replacing a List.Add() with a return yield, but a change to the way ExecuteReader worked gave a comparable change from O(n) to O(1) to get the first result. However, before then NpgsqlConnection allowed users to obtain another reader from a connection while the first was still open, and after it didn't. The docs for IDbConnection says you shouldn't do this, but that didn't mean there was no running code that did. Luckily one such piece of running code was an NUnit test, and a backwards-compatibility feature added to allow such code to continue to function with just a change to configuration.

于 2010-08-12T23:18:48.007 回答