Шаблоны или методы для модульных методов тестирования, которые вызывают статический метод
В последнее время я много размышлял о наилучшем способе "Mock" статического метода, который вызывается из класса, который я пытаюсь проверить. Возьмите следующий код, например:
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 шаблона или практики, которые могут использоваться для создания функций, которые вызывают статические функции, которые могут быть подвергнуты тестированию. К ним относятся следующие:
- Не выполняйте статическую функцию вообще и просто позвоните unit test.
- Оберните статический метод в классе экземпляра, который реализует интерфейс с функцией, которая вам нужна на нем, а затем используйте инъекцию зависимостей, чтобы использовать ее в своем классе. Я буду называть это как инъекции зависимостей интерфейса.
- Используйте Moles (или TypeMock), чтобы захватить вызов функции.
- Используйте функцию dependeny для функции. Я буду называть это как инъекции зависимостей функции.
Я слышал довольно много дискуссий о первых трех практиках, но поскольку я думал о решениях этой проблемы, четвертая идея пришла ко мне из функции зависимой инъекции. Это похоже на скрытие статической функции за интерфейсом, но без фактического создания интерфейса и класса оболочки. Примером этого может быть следующее:
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);
}
}
}
Иногда создание класса интерфейса и оболочки для вызова статической функции может быть громоздким и может загрязнять ваше решение множеством небольших классов, единственной целью которых является вызов статической функции. Я все для написания кода, который легко тестируется, но эта практика, похоже, является обходным решением для плохой среды тестирования.
Как я думал об этих различных решениях, я понял, что все четыре упомянутые выше практики могут применяться в разных ситуациях. Вот что я думаю, это правильные условия для применения вышеуказанных практик:
- Не ставьте статическую функцию, если она чисто апатридирована и не имеет доступа к системным ресурсам (например, файловой системе или базе данных). Конечно, можно аргументировать, что, если к системным ресурсам обращаются, то это все равно вводит состояние в статическую функцию.
- Используйте инъекцию зависимостей интерфейса, если вы используете несколько статических функций, которые можно логически добавить в один интерфейс. Ключевым моментом здесь является использование нескольких статических функций. Я думаю, что в большинстве случаев это не так. Вероятно, в функции будет только одна или две статические функции.
- Используйте Moles, когда вы издеваетесь над внешними библиотеками, такими как библиотеки пользовательского интерфейса или библиотеки баз данных (например, linq to sql). Мое мнение состоит в том, что если Moles (или TypeMock) используется для захвата CLR, чтобы издеваться над вашим собственным кодом, то это показатель того, что для декомпозиции объектов необходимо выполнить рефакторинг.
- Используйте инъекцию зависимостей функции, когда в тестируемом коде имеется небольшое количество вызовов статических функций. Это шаблон, к которому я склоняюсь в большинстве случаев, чтобы проверить функции, вызывающие статические функции в моих собственных классах утилиты.
Это мои мысли, но я был бы очень признателен за некоторые отзывы об этом. Каков наилучший способ проверки кода, в котором вызывается внешняя статическая функция?
Ответы
Ответ 1
Использование инъекции зависимостей (вариант 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, поэтому я не могу много говорить об этом. Мое впечатление, хотя и такое же, как у вас, что, если вам нужно его использовать, возможно, есть некоторые рефакторинги, которые можно было бы сделать.
Ответ 2
Добро пожаловать в пороки статического состояния.
Я думаю, что ваши рекомендации в порядке, в целом. Вот мои мысли:
-
Единичное тестирование любой "чистой функции", которая не вызывает побочных эффектов, отлично, независимо от видимости и объема функции. Таким образом, все те же методы, что и в статических методах тестирования модулей, таких как "Помощники Linq" и форматирование строковых строк (например, обертки для String.IsNullOrEmpty или String.Format) и другие функции утилиты без состояния.
-
Синглеты - это противник хорошего модульного тестирования. Вместо того, чтобы напрямую использовать шаблон singleton, рассмотрите возможность регистрации классов, которые вы хотите ограничить одним экземпляром, с контейнером IoC и введением их в зависимые классы. Те же преимущества, с добавленной выгодой, которую IoC можно настроить, чтобы вернуть макет в ваши проекты тестирования.
-
Если вы просто должны реализовать истинный синглтон, подумайте о том, чтобы создать конструктор по умолчанию, а не полностью закрытый, и определить "тестовый прокси", который происходит из вашего экземпляра singleton и позволяет создать объект в области экземпляра, Это позволяет генерировать "частичный макет" для любых методов, которые вызывают побочные эффекты.
-
Если ваш код ссылается на встроенную статистику (например, ConfigurationManager), которые не являются основополагающими для работы класса, либо извлеките статические вызовы в отдельную зависимость, которую вы можете высмеять, либо найдите экземпляр, основанного на решениях. Очевидно, что любая встроенная статика не тестируется на единицу, но нет никакого вреда в использовании вашей инфраструктуры модульного тестирования (MS, NUnit и т.д.) Для создания интеграционных тестов, просто держите их отдельно, чтобы вы могли запускать модульные тесты, не требуя пользовательская среда.
-
Всякий раз, когда код ссылается на статику (или имеет другие побочные эффекты), и невозможно рефакторировать в совершенно отдельный класс, извлеките статический вызов в метод и протестируйте все другие функции класса, используя "частичный макет" этот класс переопределяет метод.
Ответ 3
Просто создайте unit test для статического метода и не стесняйтесь вызывать его внутри методов для проверки без макета.
Ответ 4
Для File.Create
и MyUtilities.WriteTextToFile
я создам свою собственную оболочку и добавлю ее с помощью инъекции зависимостей. Поскольку он касается FileSystem, этот тест может замедляться из-за ввода-вывода и, возможно, даже вызывать неожиданное исключение из FileSystem, что приведет вас к мысли, что ваш класс ошибочен, но теперь.
Что касается функции MyUtilities.GetFormattedText
, я полагаю, что эта функция выполняет только некоторые изменения со строкой, здесь не о чем беспокоиться.
Ответ 5
Выбор №1 - лучший. Не издевайтесь и просто используйте статический метод, поскольку он существует. Это самый простой маршрут и делает именно то, что вам нужно. Оба сценария "инъекции" по-прежнему вызывают статический метод, поэтому вы не получаете ничего из всей дополнительной упаковки.