Як поділити тест на функцію, яка відновлена ​​до стратегії?


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(){}

Після рефакторингу у мене буде клас Фабрика NameHandlerFactoryі принаймні 3 впровадження InameHandler.

Як слід перейти до рефакторних тестів? Чи слід видалити тест одиниці claculateTax()з EmployeeTestsі створити тестовий клас для кожної реалізації InameHandler?

Чи слід перевіряти і заводський клас?

Відповіді:


6

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

Кожен з окремих випадків (на даний момент реалізованих у doSomethingспівавторах) також повинен мати власний набір тестів, які перевіряють внутрішні деталі та особливі випадки, пов'язані з кожним виконанням. У нових налаштуваннях ці тести могли / повинні бути перетворені в прямі тести відповідного класу Стратегії.

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

Оновлення

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

Я думаю, що оригінальна реалізація ваших тестів підтверджує результат конкретного розрахунку податку, неявно підтверджуючи, що конкретна стратегія обчислення була використана для її створення. Якщо ви збережете цю схему, у вас дійсно буде дублювання. Однак, як натякав @Kristof, ви можете реалізовувати тести високого рівня, використовуючи також макети, щоб лише перевірити, чи правильно обрана стратегія (макет) була обрана та використана calculateTax. У цьому випадку не буде дублювання між випробуваннями високого та низького рівня.

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

Чи слід перевіряти і заводський клас?

Знову ж таки, це залежить. Зауважимо, що тести calculateTaxефективно перевіряють фабрику. Отже, якщо заводський код є тривіальним switchблоком, як ваш код вище, ці тести можуть бути всім, що вам потрібно. Але якщо фабрика робить якісь складніші речі, можливо, ви захочете присвятити кілька тестів спеціально для цього. Все зводиться до того, скільки тестів потрібно, щоб бути впевненим, що код, про який йде мова, справді працює. Якщо, прочитавши код - або проаналізувавши дані про покриття коду - ви побачите неперевірені шляхи виконання, присвятіть ще кілька тестів для їх виконання. Потім повторюйте це, поки ви не будете повністю впевнені у своєму коді.


Я трохи змінив код, щоб він наблизився до мого фактичного практичного коду. Тепер додано другий вхід salaryдо функції calculateTax(). Таким чином, я думаю, я буду дублювати тестовий код для оригінальної функції та 3 реалізації класу стратегії.
Songo

@Songo, будь ласка, дивіться моє оновлення.
Péter Török

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()метод класу співробітника правильно запитує його NameHandlerFactoryза a, NameHandlerа потім викликає calculateTax()метод повернутого NameHandler.


hmmmm, ви маєте на увазі, що я повинен зробити тест на поведінковий тест (перевірити, чи були викликані певні функції) та зробити твердження про значення на делегованих класах?
Сонго

Так, це я і зробив. Я б справді писав окремі тести для NameHandlerFactory та NameHandler. Коли у вас є такі, немає ніяких причин знову перевіряти їх функціональність у Employee.calculateTax()методі. Таким чином, вам не потрібно додавати додаткові тести для співробітників, коли ви представляєте новий NameHandler.
Крістоф Клайс

3

Ви берете один клас (працівник, який робить все) і складаєте 3 групи класів: завод, працівник (який просто містить стратегію) та стратегії.

Тому зробіть 3 групи тестів:

  1. Випробуйте фабрику ізольовано. Чи правильно поводиться з входами. Що відбувається, коли проїдеш у невідомому?
  2. Тестуйте працівника ізольовано. Чи можете ви встановити довільну стратегію, і вона працює, як ви очікували? Що станеться, якщо немає стратегії чи заводських установок? (якщо це можливо в коді)
  3. Випробуйте стратегії ізольовано. Чи кожен виконує стратегію, яку ви очікуєте? Чи вони обробляють непарні кордонні введення послідовно?

Звичайно, ви можете зробити автоматизовані тести для цілого шебангу, але вони зараз більше схожі на інтеграційні тести і їх слід розглядати як такі.


2

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

Тоді я би реалізував Factory і продовжував би тестування для кожної реалізації та, нарешті, самих реалізацій для цих тестів.

Нарешті я видалив би старі тести.


2

На мою думку, ви нічого не повинні робити, тобто не слід додавати жодних нових тестів.

Я наголошую, що це думка, і це насправді залежить від того, як ви сприймаєте очікування від об’єкта. Як ви думаєте, користувач класу хотів би надати стратегію обчислення податку? Якщо йому все одно, то тести повинні відображати це, і поведінка, відображена в одиничних тестах, повинна бути такою, що їм не повинно було байдуже, що клас почав використовувати об'єкт стратегії для обчислення податку.

Я фактично стикався з цією проблемою кілька разів під час використання TDD. Я думаю, що головна причина полягає в тому, що об’єкт стратегії не є природною залежністю, на відміну від архітектурної граничної залежності, як зовнішній ресурс (файл, БД, віддалений сервіс тощо). Оскільки це не природна залежність, я зазвичай не базую поведінку свого класу на цій стратегії. Мій інстинкт полягає в тому, що я повинен змінити свої тести лише в тому випадку, якщо очікування від мого класу змінилися.

Там є чудовий пост дядька Боба, який говорить саме про цю проблему при використанні TDD.

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.