Нужен пример непредвиденных последствий на C#

StackOverflow https://stackoverflow.com/questions/3472312

  •  28-09-2019
  •  | 
  •  

Вопрос

Я готовлю презентацию о преимуществах модульного тестирования и хотел бы привести простой пример непредвиденных последствий:Изменение кода в одном классе, которое нарушает функциональность другого класса.

Может ли кто-нибудь предложить простой и понятный пример этого?

Мой план состоит в том, чтобы написать модульные тесты для этой функциональности, чтобы продемонстрировать, что мы знаем, что что-то сломали, немедленно запустив тест.

Это было полезно?

Решение

Слегка проще, и, следовательно, яснее, пример:

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[], Это то, что я также видел в производственном коде. Излишне говорить, что это больше нет!

Другие советы

Вот пример:

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

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

Изменять GetData() вернуть А. List<Something>, а также Consumer сломает.

Это могло видеть несколько увольненным, но я видел подобные проблемы в реальном коде.

Допустим, у вас есть метод, который делает:

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() возвращаемым выходом, но изменение способа работы ExecuteReader дало сопоставимое изменение с O(n) на O(1) для получения первого результата.Однако до этого NpgsqlConnection позволял пользователям получать другое устройство чтения из соединения, пока первое было еще открыто, а после этого этого не происходило.В документации IDbConnection говорится, что этого делать не следует, но это не означает, что не было работающего кода, который мог бы это сделать.К счастью, одним из таких фрагментов работающего кода был тест NUnit, и в него была добавлена ​​функция обратной совместимости, позволяющая такому коду продолжать функционировать, просто изменив конфигурацию.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top