Подтвердить что ты не робот

Шаблоны или методы для модульных методов тестирования, которые вызывают статический метод

В последнее время я много размышлял о наилучшем способе "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, чтобы издеваться над вашим собственным кодом, то это показатель того, что для декомпозиции объектов необходимо выполнить рефакторинг.
  • Используйте инъекцию зависимостей функции, когда в тестируемом коде имеется небольшое количество вызовов статических функций. Это шаблон, к которому я склоняюсь в большинстве случаев, чтобы проверить функции, вызывающие статические функции в моих собственных классах утилиты.

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

4b9b3361

Ответ 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 - лучший. Не издевайтесь и просто используйте статический метод, поскольку он существует. Это самый простой маршрут и делает именно то, что вам нужно. Оба сценария "инъекции" по-прежнему вызывают статический метод, поэтому вы не получаете ничего из всей дополнительной упаковки.