2010-08-12 3 views
8

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

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

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

+0

Является ли этот вопрос языком агностиком? – kbrimington

+0

@kbrimington C#. –

+0

C# является предпочтительным, потому что это моя аудитория; Но я могу переписать его на C#, учитывая хороший, простой пример. – dgiard

ответ

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

+0

Как бы вы могли это проверить? Изменение возвращаемого значения можно поймать во время компиляции (например, не может быть выполнено «String address = GetServerAddreess();'), но улов в Строках почти невозможно – TheLQ

+0

@TheLQ, если вы код, а не: 'string serverAddress = GetServerAddress(); Console.WriteLine («Адрес сервера:« + serverAddress »), вы получите ошибку компиляции в этом примере =) И нет никакого смысла беспокоиться о том, что« вербальный код менее эффективен », как если бы JIT не смог оптимизировать это , Я был бы очень удивлен * И * обеспокоен! :-) – Rob

8

Вот пример:

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

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

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

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

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

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


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