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

10

Если у меня есть функция в моем коде, которая выглядит следующим образом:

class Employee{

    public string calculateTax(string name, int salary)
    {
        switch (name)
        {
            case "Chris":
                doSomething($salary);
            case "David":
                doSomethingDifferent($salary);
            case "Scott":
               doOtherThing($salary);               
       }
}

Обычно я реорганизовал бы это, чтобы использовать Ploymorphism, используя фабричный класс и шаблон стратегии:

public string calculateTax(string name)
{
    InameHandler nameHandler = NameHandlerFactory::getHandler(name);
    nameHandler->calculateTax($salary);
}

Теперь, если бы я использовал TDD, у меня были бы некоторые тесты, которые работали бы с оригиналом calculateTax()перед рефакторингом.

например:

calculateTax_givenChrisSalaryBelowThreshold_Expect111(){}    
calculateTax_givenChrisSalaryAboveThreshold_Expect111(){}

calculateTax_givenDavidSalaryBelowThreshold_Expect222(){}   
calculateTax_givenDavidSalaryAboveThreshold_Expect222(){} 

calculateTax_givenScottSalaryBelowThreshold_Expect333(){}
calculateTax_givenScottSalaryAboveThreshold_Expect333(){}

После рефакторинга у меня будет класс Factory NameHandlerFactoryи как минимум 3 реализации InameHandler.

Как мне провести рефакторинг моих тестов? Должен ли я удалить модульный тест для claculateTax()from EmployeeTestsи создать класс Test для каждой реализации InameHandler?

Должен ли я также проверить класс Factory?

Songo
источник

Ответы:

6

Старые тесты отлично подходят для проверки того, что calculateTaxвсе еще работает как надо. Однако для этого вам не нужно много тестовых случаев, только 3 (или, может быть, еще несколько, если вы тоже хотите протестировать обработку ошибок, используя неожиданные значения name).

Каждый из отдельных случаев (в настоящее время реализованный в doSomethingи др.) Также должен иметь свой собственный набор тестов, которые проверяют внутренние детали и особые случаи, связанные с каждой реализацией. В новой настройке эти тесты могут / должны быть преобразованы в прямые тесты для соответствующего класса Стратегии.

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

Обновить

Может быть некоторое дублирование между тестами calculateTax(назовем их тестами высокого уровня ) и тестами для отдельных стратегий вычислений ( тестами низкого уровня ) - это зависит от вашей реализации.

Я предполагаю, что оригинальная реализация ваших тестов утверждает результат конкретного расчета налога, неявно проверяя, использовалась ли конкретная стратегия расчета для его получения. Если вы сохраните эту схему, у вас действительно будет дублирование. Однако, как намекнул @Kristof, вы можете реализовать высокоуровневые тесты, также используя mocks, чтобы проверить только то, что правильная (mock) стратегия была выбрана и запущена calculateTax. В этом случае не будет дублирования между тестами высокого и низкого уровня.

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

Должен ли я также проверить класс Factory?

Опять же, это зависит. Обратите внимание, что тесты calculateTaxэффективно тестируют на заводе. Так что, если заводской код является тривиальным switchблоком, как ваш код выше, эти тесты могут быть всем, что вам нужно. Но если фабрика делает некоторые более сложные вещи, вы можете посвятить некоторые тесты специально для этого. Все сводится к тому, сколько тестов вам нужно, чтобы быть уверенным, что рассматриваемый код действительно работает. Если после прочтения кода или анализа данных покрытия кода вы увидите непроверенные пути выполнения, выделите еще несколько тестов для их выполнения. Затем повторяйте это, пока вы не будете полностью уверены в своем коде.

Петер Тёрёк
источник
Я немного изменил код, чтобы приблизить его к моему практическому коду. Теперь второй вход salaryв функцию calculateTax()был добавлен. Таким образом, я думаю, я буду дублировать тестовый код для исходной функции и трех реализаций класса стратегии.
Сонго
@ Сонго, пожалуйста, смотрите мое обновление.
Петер Тёрёк
5

Начну с того, что я не эксперт по TDD или модульному тестированию, но вот как я это протестирую (я буду использовать псевдоподобный код):

CalculateTaxDelegatesToNameHandler()
{
    INameHandlerFactory fakeNameHandlerFactory = Fake(INameHandlerFactory);
    INameHandler fakeNameHandler = Fake(INameHandler);

    A.Call.To(fakeNameHandlerFactory.getHandler("John")).Returns(fakeNameHandler);

    Employee employee = new Employee(fakeNameHandlerFactory);
    employee.CalculateTax("John");

    Assert.That.WasCalled(fakeNameHandler.calculateTax());
}

Поэтому я бы проверил, что calculateTax()метод класса employee правильно запрашивает его NameHandlerFactoryдля a, NameHandlerа затем вызывает calculateTax()метод возвращаемого значения NameHandler.

Кристоф Клас
источник
хмммм, так что вы имеете в виду, что я должен вместо этого сделать тест поведенческим тестом (проверяющим, что определенные функции были вызваны) и сделать утверждения значений в делегированных классах?
Сонго
Да, я бы так и сделал. Я бы действительно написал отдельные тесты для NameHandlerFactory и NameHandler. Когда они у вас есть, нет смысла снова проверять их функциональность в Employee.calculateTax()методе. Таким образом, вам не нужно добавлять дополнительные тесты Employee, когда вы вводите новый NameHandler.
Кристоф Клас
3

Вы берете один класс (сотрудник, который делает все) и делаете 3 группы классов: фабрика, сотрудник (который просто содержит стратегию) и стратегии.

Итак, сделайте 3 группы тестов:

  1. Проверьте завод в изоляции. Правильно ли он обрабатывает вводимые данные? Что происходит, когда вы проходите мимо неизвестного?
  2. Проверьте работника в изоляции. Можете ли вы установить произвольную стратегию, и она работает так, как вы ожидаете? Что произойдет, если нет стратегии или заводских настроек? (если это возможно в коде)
  3. Проверьте стратегии в изоляции. Каждый выполняет стратегию, которую вы ожидаете? Они обрабатывают нечетные граничные входы согласованным образом?

Конечно, вы можете делать автоматические тесты для всего shebang, но теперь они больше похожи на интеграционные тесты и должны рассматриваться как таковые.

Telastyn
источник
2

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

Затем я реализую Factory и продолжу тестирование для каждой реализации и, наконец, сами реализации для этих тестов.

Наконец я бы удалил старые тесты.

Паткос Чаба
источник
2

Мое мнение таково, что вы ничего не должны делать, то есть не должны добавлять новые тесты.

Я подчеркиваю, что это мнение, и оно на самом деле зависит от того, как вы воспринимаете ожидания от объекта. Как вы думаете, пользователь класса хотел бы предоставить стратегию для расчета налога? Если ему все равно, то тесты должны отражать это, а поведение, отраженное в модульных тестах, должно заключаться в том, что им не нужно заботиться о том, чтобы класс начал использовать объект стратегии для расчета налога.

Я действительно сталкивался с этой проблемой несколько раз при использовании TDD. Я думаю, что основная причина в том, что объект стратегии не является естественной зависимостью, в отличие от, скажем, архитектурно-граничной зависимости, такой как внешний ресурс (файл, БД, удаленный сервис и т. Д.). Поскольку это не естественная зависимость, я обычно не основываю поведение своего класса на этой стратегии. Мой инстинкт состоит в том, что я должен менять свои тесты, только если ожидания от моего класса изменились.

Есть отличный пост от дяди Боба, в котором рассказывается именно об этой проблеме при использовании TDD.

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

Рафи Голдфарб
источник