【问题标题】:Need a C# example of unintended consequences需要一个意外后果的 C# 示例
【发布时间】:2011-03-29 04:33:19
【问题描述】:

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

有人能推荐一个简单易懂的例子吗?

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

【问题讨论】:

  • 这个问题与语言无关吗?
  • C# 是首选,因为那是我的听众的;但是我可以用 C# 重写它,给出一个好的、简单的例子。
  • +1 来自我。有机会分享你整理的演示文稿吗?我认为它非常有价值,并且有我想展示给人们看的人 =)

标签: c# unit-testing demo


【解决方案1】:

假设你有一个方法:

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 个名称的情况,如果会带来安全风险,情况可能会更糟)。

任何在 CheckNames 上命中且结果大于零的单元测试都会捕获它。


顺便提一下,类似的(如果更复杂的话)更改是 NPGSQL 中向后兼容功能的原因。并不像仅仅将 List.Add() 替换为 return yield 那样简单,而是对 ExecuteReader 工作方式的改变给出了从 O(n) 到 O(1) 的类似变化,以获得第一个结果。然而,在此之前,NpgsqlConnection 允许用户在第一个仍然打开时从连接中获取另一个阅读器,之后它没有。 IDbConnection 的文档说您不应该这样做,但这并不意味着没有运行代码可以这样做。幸运的是,这样的一段运行代码是 NUnit 测试,并添加了向后兼容功能,以允许此类代码只需更改配置即可继续运行。

【讨论】:

    【解决方案2】:

    一个稍微简单,因此也许更清晰的例子是:

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

    【讨论】:

    • 你到底会如何测试呢?可以在编译时捕获更改返回值(例如不能做String address = GetServerAddreess();),但在字符串中捕获几乎是不可能的
    • @TheLQ,如果你的代码是:string serverAddress = GetServerAddress(); Console.WriteLine("Server address is: " + serverAddress); 在这个例子中你会得到一个编译错误 =) 而且,没有必要担心“冗长的代码效率较低”,好像JIT 未能优化它,我会非常惊讶并且 担心! :-)
    【解决方案3】:

    这是一个例子:

    class DataProvider {
        public static IEnumerable<Something> GetData() {
            return new Something[] { ... };
        }
    }
    
    class Consumer {
        void DoSomething() {
            Something[] data = (Something[])DataProvider.GetData();
        }
    }
    

    GetData() 更改为返回List&lt;Something&gt;Consumer 将中断。

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

    【讨论】:

      猜你喜欢
      • 2019-10-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-07-04
      • 2018-05-08
      • 1970-01-01
      • 1970-01-01
      • 2011-09-03
      相关资源
      最近更新 更多