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

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

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

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

8
задан Rob 5 September 2010 в 00:07
поделиться

3 ответа

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

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

12
ответ дан 5 December 2019 в 06:52
поделиться

Вот пример:

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

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

Измените GetData () , чтобы он возвращал Список , и Потребитель сломается.

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

8
ответ дан 5 December 2019 в 06:52
поделиться

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

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, либо перечислит через enumerable, после чего будет произведена проверка имени.

Кто-то хотя и видит свойство 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, в который была добавлена ​​функция обратной совместимости, позволяющая такому коду продолжать работать с простым изменением конфигурации.

4
ответ дан 5 December 2019 в 06:52
поделиться
Другие вопросы по тегам:

Похожие вопросы: