Нужен пример непредвиденных последствий на C # - PullRequest
8 голосов
/ 13 августа 2010

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

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

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

Ответы [ 3 ]

12 голосов
/ 13 августа 2010

Немного более простой и, следовательно, возможно более понятный пример:

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

8 голосов
/ 13 августа 2010

Вот пример:

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

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

Измените GetData(), чтобы вернуть List<Something>, и Consumer сломается.

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

4 голосов
/ 13 августа 2010

Скажем, у вас есть метод, который делает:

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 выдаст ошибку (и, возможно, вернет ложный результат в случай с более чем одним именем, что может быть еще хуже, если это создает угрозу безопасности).

Любой юнит-тест, который попадет в CheckNames с более чем нулевым результатом, все равно его поймает.


Кстати, сопоставимое (если более сложное) изменение является причиной обратной совместимости в NPGSQL. Не так просто, как простая замена List.Add () возвращаемым доходом, но изменение способа работы ExecuteReader дало сопоставимое изменение с O (n) на O (1), чтобы получить первый результат. Однако до этого NpgsqlConnection позволял пользователям получать другое устройство чтения из соединения, когда первое было еще открыто, а после этого не было. Документы для IDbConnection говорят, что вы не должны этого делать, но это не значит, что не было запущенного кода, который бы это делал. К счастью, одним из таких фрагментов выполняемого кода был тест NUnit, и была добавлена ​​функция обратной совместимости, позволяющая такому коду продолжать функционировать, просто изменив конфигурацию.

...