需要一个意想不到的后果的C#示例

时间:2010-08-12 21:45:53

标签: c# unit-testing demo

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

有人可以建议一个简单,易于解释的例子吗?

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

3 个答案:

答案 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测试,并且添加了向后兼容性功能,允许这些代码只需更改配置即可继续运行。