Шаблоны или методы для методов модульного тестирования, которые вызывают статический метод - PullRequest
32 голосов
/ 31 марта 2011

В последнее время я много размышлял о том, как наилучшим образом «смоделировать» статический метод, вызываемый из класса, который я пытаюсь протестировать. Возьмите следующий код, например:

using (FileStream fStream = File.Create(@"C:\test.txt"))
{
    string text = MyUtilities.GetFormattedText("hello world");
    MyUtilities.WriteTextToFile(text, fStream);
}

Я понимаю, что это довольно плохой пример, но он имеет три статических вызова методов, которые немного отличаются друг от друга. Функция File.Create обращается к файловой системе, и мне не принадлежит эта функция. MyUtilities.GetFormattedText - это функция, которой я владею, и она не содержит состояний. Наконец, MyUtilities.WriteTextToFile - это принадлежащая мне функция, которая обращается к файловой системе.

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

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

  1. Не надо насмехаться над статической функцией, просто позвольте ее вызывать из модульного теста.
  2. Оберните статический метод в классе экземпляра, который реализует интерфейс с нужной вам функцией, а затем используйте внедрение зависимостей, чтобы использовать его в своем классе. Я буду ссылаться на это как внедрение зависимости интерфейса .
  3. Используйте Кроты (или TypeMock) для перехвата вызова функции.
  4. Использовать зависимости зависимости для функции. Я буду ссылаться на это как внедрение зависимости функции .

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

public class MyInstanceClass
{
    private Action<string, FileStream> writeFunction = delegate { };

    public MyInstanceClass(Action<string, FileStream> functionDependency)
    {
        writeFunction = functionDependency;
    }

    public void DoSomething2()
    {
        using (FileStream fStream = File.Create(@"C:\test.txt"))
        {
            string text = MyUtilities.GetFormattedText("hello world");
            writeFunction(text, fStream);
        }
    }
}

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

Размышляя об этих разных решениях, я пришел к выводу, что все 4 вышеупомянутых практики можно применять в разных ситуациях. Вот что я думаю о правильных обстоятельствах для применения вышеуказанных практик :

  1. Не высмеивать статическую функцию, если она не имеет состояния и не имеет доступа к системным ресурсам (таким как файловая система или база данных). Конечно, можно утверждать, что если к системным ресурсам обращаются, то это все равно вводит состояние в статическую функцию.
  2. Используйте внедрение зависимостей интерфейса , когда вы используете несколько статических функций, которые можно логически добавить в один интерфейс. Ключевым моментом здесь является то, что используется несколько статических функций. Я думаю, что в большинстве случаев это будет не так. Возможно, в функции будет вызываться только одна или две статические функции.
  3. Используйте Moles , когда вы копируете внешние библиотеки, такие как библиотеки пользовательского интерфейса или библиотеки баз данных (например, linq to sql). Мое мнение таково, что если Moles (или TypeMock) используется для захвата CLR с целью имитации вашего собственного кода, то это показатель того, что для разъединения объектов необходим некоторый рефакторинг.
  4. Используйте внедрение зависимости функции , когда в тестируемом коде имеется небольшое количество вызовов статических функций. Это шаблон, к которому я склоняюсь в большинстве случаев для тестирования функций, вызывающих статические функции, в моих собственных служебных классах.

Это мои мысли, но я был бы очень признателен за отзыв. Как лучше всего тестировать код, когда вызывается внешняя статическая функция?

Ответы [ 5 ]

12 голосов
/ 31 марта 2011

Использование внедрения зависимостей (вариант 2 или 4), безусловно, является моим предпочтительным методом атаки на это.Это не только облегчает тестирование, но и помогает разделить проблемы и не допустить раздувания классов.

Я хочу пояснить, что неверно то, что статические методы сложно тестировать.Проблема со статическими методами возникает, когда они используются в другом методе.Это затрудняет проверку метода, вызывающего статический метод, так как статический метод не может быть смоделирован.Обычный пример этого с I / O.В вашем примере вы пишете текст в файл (WriteTextToFile).Что если что-то не получится во время этого метода?Так как метод является статическим и его нельзя смоделировать, вы не можете по требованию создавать случаи, такие как случаи сбоев.Если вы создаете интерфейс, то вы можете посмеяться над вызовом WriteTextToFile и заставить его имитировать ошибки.Да, у вас будет еще несколько интерфейсов и классов, но обычно вы можете логически сгруппировать похожие функции в один класс.

Без внедрения зависимостей: Это в значительной степени вариант 1, где ничего не высмеивается,Я не рассматриваю это как надежную стратегию, потому что она не позволяет вам тщательно протестировать.

public void WriteMyFile(){
    try{
        using (FileStream fStream = File.Create(@"C:\test.txt")){
            string text = MyUtilities.GetFormattedText("hello world");
            MyUtilities.WriteTextToFile(text, fStream);
        }
    }
    catch(Exception e){
        //How do you test the code in here?
    }
}

с внедрением зависимости:

public void WriteMyFile(IFileRepository aRepository){
    try{
        using (FileStream fStream = aRepository.Create(@"C:\test.txt")){
            string text = MyUtilities.GetFormattedText("hello world");
            aRepository.WriteTextToFile(text, fStream);
        }
    }
    catch(Exception e){
        //You can now mock Create or WriteTextToFile and have it throw an exception to test this code.
    }
}

НаС другой стороны, хотите ли вы, чтобы ваши тесты бизнес-логики не сработали, если файловая система / база данных не могут быть прочитаны / записаны?Если мы проверяем правильность математики при расчете зарплаты, мы не хотим, чтобы ошибки ввода-вывода приводили к провалу теста.

Без внедрения зависимости:

Это немного странный пример / метод, но я использую его только для иллюстрации своей точки.

public int GetNewSalary(int aRaiseAmount){
    //Do you really want the test of this method to fail because the database couldn't be queried?
    int oldSalary = DBUtilities.GetSalary(); 
    return oldSalary + aRaiseAmount;
}

С впрыском зависимости:

public int GetNewSalary(IDBRepository aRepository,int aRaiseAmount){
    //This call can now be mocked to always return something.
    int oldSalary = aRepository.GetSalary();
    return oldSalary + aRaiseAmount;
}

Повышенная скорость - дополнительный бонус насмешки.IO является дорогостоящим, и сокращение IO увеличит скорость ваших тестов.Отсутствие необходимости ждать транзакции базы данных или функции файловой системы улучшит производительность ваших тестов.

Я никогда не использовал TypeMock, поэтому не могу много говорить об этом.Впрочем, у меня такое же впечатление, как и у вас, что, если вам придется его использовать, возможно, придется провести рефакторинг.

10 голосов
/ 31 марта 2011

Добро пожаловать в зло статического состояния.

Я думаю, что ваши рекомендации в целом нормальные.Вот мои мысли:

  • Модульное тестирование любой «чистой функции», которая не вызывает побочных эффектов, прекрасно, независимо от видимости и области действия функции.Итак, юнит-тестирование статических методов расширения, таких как "помощники Linq" и форматирование строки (например, обертки для String.IsNullOrEmpty или String.Format) и других служебных функций без сохранения состояния, все хорошо.

  • Синглтоны - враг хорошего юнит-тестирования.Вместо непосредственной реализации одноэлементного шаблона, рассмотрите возможность регистрации классов, которые вы хотите ограничить одним экземпляром, с контейнером IoC и внедрения их в зависимые классы.Те же преимущества, с дополнительным преимуществом, что IoC может быть настроен на возврат макета в ваших проектах тестирования.

  • Если вам просто нужно реализовать настоящий синглтон, рассмотрите возможность сделать защищенный конструктор по умолчанию вместо этого.полностью приватный, и определите «тестовый прокси», который получен из вашего экземпляра синглтона и позволяет создавать объект в области видимости экземпляра.Это позволяет генерировать «частичное макетирование» для любых методов, которые вызывают побочные эффекты.

  • Если ваш код ссылается на встроенную статику (такую ​​как ConfigurationManager), которая не является фундаментальной дляПри работе с классом либо извлекайте статические вызовы в отдельную зависимость, которую вы можете смоделировать, либо ищите решение на основе экземпляра.Очевидно, что любая встроенная статика не подлежит модульному тестированию, но использование вашей инфраструктуры модульного тестирования (MS, NUnit и т. Д.) Для создания интеграционных тестов не повредит, просто держите их отдельно, чтобы вы могли запускать модульные тесты без необходимостипользовательская среда.

  • Везде, где код ссылается на статику (или имеет другие побочные эффекты), и невозможно выполнить рефакторинг в совершенно отдельный класс, извлечь статический вызов в метод и протестировать все остальныефункциональность класса с использованием «частичного макета» этого класса, который переопределяет метод.

1 голос
/ 31 марта 2011

Для File.Create и MyUtilities.WriteTextToFile я бы создал свою собственную оболочку и вставил бы ее с помощью инъекции зависимостей. Поскольку этот процесс затрагивает FileSystem, этот тест может замедлиться из-за операций ввода-вывода и, возможно, даже вызвать неожиданное исключение из FileSystem, которое заставит вас думать, что ваш класс неверен, но теперь это так.

Что касается функции MyUtilities.GetFormattedText, я полагаю, что эта функция только вносит некоторые изменения в строку, здесь не о чем беспокоиться.

1 голос
/ 31 марта 2011

Просто создайте модульный тест для статического метода и не стесняйтесь вызывать его внутри методов для тестирования, не издавая его.

0 голосов
/ 31 марта 2011

Выбор № 1 самый лучший. Не издевайтесь, а просто используйте статический метод, как он существует. Это самый простой маршрут, и он делает именно то, что вам нужно. Оба ваших сценария «инъекции» по-прежнему вызывают статический метод, поэтому вы ничего не получаете от всей дополнительной упаковки.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...