Написання мінімального коду для проходження одиничного тесту - без обману!


36

Коли ви робите TDD і пишете тестовий блок, як протистояти бажанням "обдурити" під час написання першої ітерації коду "впровадження", який ви тестуєте?

Наприклад:
Давайте мені потрібно обчислити Фактор числа. Я починаю з одиничного тестування (використовуючи MSTest) приблизно так:

[TestClass]
public class CalculateFactorialTests
{
    [TestMethod]
    public void CalculateFactorial_5_input_returns_120()
    {
        // Arrange
        var myMath = new MyMath();
        // Act
        long output = myMath.CalculateFactorial(5);
        // Assert
        Assert.AreEqual(120, output);
    }
}

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

Справа в тому, що я постійно спокушаюся написати наступне:

public class MyMath
{
    public long CalculateFactorial(long input)
    {
        return 120;
    }
}

Це технічно правильно, оскільки це дійсно мінімальний код, необхідний для того, щоб зробити цей тестовий пропуск (зелений колір), хоча це, очевидно, "обман", оскільки він насправді навіть не намагається виконувати функцію обчислення факторіалу. Зрозуміло, тепер частина рефакторингу стає вправою "написання правильної функціональності", а не справжнім рефакторингом реалізації. Очевидно, що додавання додаткових тестів з різними параметрами не вдасться і не призведе до рефакторингу, але вам потрібно почати з цього одного тесту.

Отже, моє запитання полягає в тому, як ви отримуєте цей баланс між "написанням мінімального коду для проходження тесту", при цьому він залишається функціональним і в дусі того, що ви насправді намагаєтесь досягти?


4
Це людська річ: ти повинен чинити опір бажанням обманювати. Більше нічого немає. Ви можете додати більше тестів і написати більше тестового коду, ніж код для тестування, але якщо у вас немає такої розкоші, то вам потрібно буде просто чинити опір. Є багато місць у кодуванні, де нам доводиться протистояти прагненню зламати чи обманювати, тому що ми знаємо, що, хоча це може працювати сьогодні, він не пізніше працює.
Dan Rosenstark

7
Звичайно, в TDD це робити навпаки - це обман - тобто повернення 120 - це правильний шлях. Мені досить складно змусити себе це робити, а не забігати вперед і почати писати факторський розрахунок.
Пол Різник

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

3
Якщо виявиться, що код клієнтського коду тільки коли-небудь передається у 5, повернення 120 - це не просто нечіткий підхід, але насправді є законним рішенням.
Відновити Крамію Моніку

Я погоджуюся з @PaulButcher - насправді, багато прикладів тестування одиниць у текстах та статтях брали б такий підхід.
HorusKol

Відповіді:


45

Це ідеально законно. Червоний, зелений, Refactor.

Перший тест проходить.

Додайте другий тест, з новим входом.

Тепер швидко перейдіть до зеленого кольору, ви можете додати інше, що працює чудово. Це проходить, але ви ще не закінчили.

Третя частина Червоного, Зеленого, Рефактора є найважливішою. Refactor для видалення дублювання . Зараз у вас буде дублювання коду. Два заяви, що повертають цілі числа. І єдиний спосіб видалити це дублювання - це правильно кодувати функцію.

Я не кажу, що не пишу це правильно з першого разу. Я просто кажу, що це не обман, якщо ви цього не зробите.


12
Це просто викликає питання, чому б не просто написати функцію в першу чергу правильно?
Роберт Харві

8
@ Роберт, фактичні числа тривіально прості. Справжня перевага TDD полягає в тому, що ви пишете нетривіальні бібліотеки, а написання тесту спочатку змушує вас розробити API перед реалізацією, що - на мій досвід - призводить до кращого коду.

1
@Robert, саме ви стурбовані вирішенням проблеми замість здачі тесту. Я кажу вам, що для нетривіальних проблем просто краще відкласти жорстку конструкцію, поки у вас не з’являться тести.

1
@ Thorbjørn Равн Андерсен, ні, я не кажу, що ти можеш мати лише одне повернення. Існують поважні причини для декількох (тобто тверджень про охорону). Справа в тому, що обидві декларації повернення були "рівними". Вони зробили те ж саме. Вони просто мали різні значення. TDD не стосується жорсткості та дотримання певного розміру співвідношення тесту / коду. Йдеться про створення рівня комфорту в базі коду. Якщо ви можете написати невдалий тест, то функція, яка буде працювати для майбутніх тестів цієї функції, чудова. Зробіть це, тоді напишіть кращі тести, щоб ваша функція все ще працювала.
CaffGeek

3
Сенс не писати одразу повного (хоч і простого) впровадження полягає в тому, що тоді ви взагалі не маєте гарантії, що ваші тести навіть МОЖУТЕ провалитись. Точка бачення тесту не вдається перед тим, як пройти її, це те, що ви маєте фактичне підтвердження того, що ваша зміна коду задовольнила твердження, яке ви зробили на ньому. це єдина причина, чому TDD настільки чудовий для створення набору тестів на регресію і повністю витирає підлогу "тестом після" - в цьому сенсі.
сара

25

Очевидно розуміння кінцевої мети та досягнення алгоритму, який відповідає цій цілі.

TDD - не чарівна куля для дизайну; ви все ще повинні знати, як вирішити проблеми за допомогою коду, і ви все ще повинні знати, як це зробити на рівні, вищому за кілька рядків коду, щоб зробити тестовий пропуск.

Мені подобається ідея TDD, оскільки вона заохочує гарний дизайн; це змушує задуматися над тим, як ви можете написати свій код, щоб він був перевіреним, і загалом ця філософія підштовхне код до кращого дизайну в цілому. Але ви все ще повинні знати, як архітектурно вирішити.

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

Дядько Боб Мартін говорить так:

Якщо ви не займаєтеся розробкою тестових програм, назвати себе професіоналом дуже важко. Джим Коплін покликав мене на килим для цього. Йому не сподобалось, що я це сказав. Насправді, його позиція зараз полягає в тому, що Test Driven Development знищує архітектури, оскільки люди пишуть тести на відмову від будь-якого іншого виду думок і розривають їх архітектури в шаленому поспіху, щоб отримати тести, і він отримав цікавий момент, це цікавий спосіб зловживати ритуалом і втратити наміри, що стоять за дисципліною.

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

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


Насправді не відповідь на питання, але 1+
Ніхто

2
@rmx: Гм, питання полягає в тому, як ви отримуєте цей баланс між "написанням мінімального коду для проходження тесту", при цьому він залишається функціональним і в дусі того, що ви насправді намагаєтесь досягти? Чи читаємо ми те саме запитання?
Роберт Харві

Ідеальне рішення - алгоритм і не має нічого спільного з архітектурою. Виконання TDD не змусить вигадувати алгоритми. У якийсь момент вам потрібно зробити кроки щодо алгоритму / рішення.
Джоппе

Я згоден з @rmx. Це насправді не відповідає моєму конкретному запитанню, але саме по собі дає поживу для роздумів про те, як TDD взагалі вписується у загальну картину загального процесу розробки програмного забезпечення. Отже, з цієї причини +1.
CraigTP

Я думаю, ви могли б замінити "алгоритми" - та інші терміни - на "архітектура", і аргумент все ще зберігається; справа в тому, що неможливо побачити деревину для дерев. Якщо ви не збираєтеся написати окремий тест для кожного цілого цілого вводу, TDD не зможе розрізнити належну факторну реалізацію та деяке збочене жорстке кодування, яке працює для всіх перевірених випадків, але не для інших. Проблема з TDD полягає в простоті, з якою "всі тести проходять" і "код хороший" пов'язані. У якийсь момент потрібно застосувати важку міру здорового глузду.
Джулія Хейвард

16

Дуже гарне запитання ... і я повинен не погодитися майже з усіма, крім @Robert.

Написання

return 120;

бо факторна функція зробити один тестовий пропуск - це марна трата часу . Це не "обман", а також не слідування червоно-зеленим-рефактором буквально. Це неправильно .

Ось чому:

  • Обчислити Фактор - це особливість, а не "повернути константу". "повернення 120" не є розрахунком.
  • аргументи «рефактора» неправильно керуються; якщо у вас є два випадки випробування для 5 і 6, цей код все ще не так, тому що ви не обчислення факторіала на все :

    if (input == 5) { return 120; } //input=5 case
    else { return 720; }   //input=6 case
    
  • якщо ми дотримуємось аргументу «рефактор» буквально , тоді, коли у нас є 5 тестових випадків, ми б викликали YAGNI та реалізували функцію за допомогою таблиці пошуку:

    if (factorialDictionary.Contains(input)) {
        return factorialDictionary[input]; 
    }
    throw new Exception("Input failure");
    

Жоден з них не на самому ділі обчислень нічого, ви . І це не завдання!


1
@rmx: ні, не пропустив; "Рефактор для видалення дублювання" може бути задоволений таблицею пошуку. BTW принцип того, що одиничні тести кодують вимоги не є специфічним для BDD, це загальний принцип Agile / XP. Якщо вимога була "Дайте відповідь на запитання" що таке фактор 5 ", то" поверніть 120; " було б законно ;-)
Стівен А. Лоу

2
@Chad все, що зайва робота - просто напишіть функцію вперше ;-)
Стівен А. Лоу

2
@Steven A.Lowe, за цією логікою, навіщо писати тести ?! "Просто напишіть заявку перший раз!" Суть TDD - це невеликі, безпечні, покрокові зміни.
CaffGeek

1
@Chad: солом’яник.
Стівен А. Лоу

2
Сенс не писати одразу повного (хоч і простого) впровадження полягає в тому, що тоді ви взагалі не маєте гарантії, що ваші тести навіть МОЖУТЕ провалитись. Точка бачення тесту не вдається перед тим, як пройти її, це те, що ви маєте фактичне підтвердження того, що ваша зміна коду задовольнила твердження, яке ви зробили на ньому. це єдина причина, чому TDD настільки чудовий для створення набору тестів на регресію і повністю витирає підлогу "тестом після" - в цьому сенсі. Ви ніколи не випадково пишете тест, який не може провалитися. також погляньте на ката-дяді Бобса.
сара

10

Коли ви написали лише один одиничний тест, однорядкова реалізація ( return 120;) є законною. Написати цикл, що обчислює значення 120 - це було б обманом!

Такі прості початкові тести - хороший спосіб зафіксувати кращі справи та запобігти разовим помилкам. П'ять насправді не є вхідним значенням, з якого я б почав.

Основне правило, яке може бути тут корисним: нуль, один, багато, партій . Нуль і один - важливі крайові випадки для факторіалу. Вони можуть бути реалізовані однолінійними. Тестовий випадок "багато" (наприклад, 5!) Змусив би вас написати цикл. Тестовий випадок "lot" (1000 !?) може змусити вас застосувати альтернативний алгоритм для обробки дуже великих чисел.


2
Справа "-1" була б цікавою. Оскільки це не чітко визначено, тож і хлопець, який пише тест, і хлопець, що пише код, повинні спершу погодитись, що має статися.
gnasher729

2
+1 за те, що насправді вказує, що factorial(5)це поганий перший тест. ми починаємо з найпростіших можливих випадків, і в кожній ітерації ми робимо тести трохи більш конкретними, закликаючи код стати трохи більш загальним. це те, що дядько Боб називає передумовою пріоритетності трансформації ( blog.8thlight.com/uncle-bob/2013/05/27/… )
sara

5

Поки у вас є лише один тест, мінімально необхідний код, необхідний для проходження тесту, є справді return 120;, і ви можете легко зберегти його до тих пір, поки у вас більше не буде тестів.

Це дозволяє відкласти подальшу розробку, поки ви фактично не напишіть тести, які виконують ІНШІ зворотні значення цього методу.

Будь ласка, пам’ятайте, що тест - це версія, яку можна виконати у вашій специфікації, і якщо все, що зазначено в специфікації, це f (6) = 120, то це цілком відповідає рахунку.


Серйозно? За цією логікою вам доведеться переписувати код щоразу, коли хтось придумає новий ввід.
Роберт Харві

6
@Robert, в деякому пункті додавання нового випадку більше не призведе до найпростішого можливого коду, після чого ви пишете нову реалізацію. Оскільки ви вже встановили тести, ви точно знаєте, коли ваша нова реалізація робить те саме, що і стара.

1
@ Thorbjørn Ravn Andersen, саме, найважливіша частина Red-Green-Refactor - це рефакторинг.
CaffGeek

+1: Це також загальна ідея, наскільки я знаю, але щось потрібно сказати про виконання контракту, що мається на увазі (тобто фактор ім'я методу ). Якщо ви коли-небудь спец (тобто тест) f (6) = 120, тоді вам потрібно лише повернути 120. Як тільки ви почнете додавати тести, щоб переконатися, що f (x) == x * x-1 ... * xx-1: topBound> = x> = 0, ви отримаєте функцію, яка задовольняє факторне рівняння.
Стівен Еверс

1
@SnOrfus, місце для "підрядних контрактів" є у тестових випадках. Якщо контракт на факторіали, ви TEST , якщо відомі факториалов є і , якщо відомо , що не є факторіали не є. Їх безліч. Не потрібно багато часу, щоб перетворити список десяти перших фабрик для тестування циклу кожного номера до десятого фактора.

4

Якщо ви в змозі "обдурити" таким чином, це говорить про те, що ваші одиничні тести є помилковими.

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

Розглядайте свої тести на одиницю як прояв вимог - вони повинні спільно визначати поведінку методу, який вони перевіряють. (Це відомо як розвиток, орієнтований на поведінку - його майбутнє ;-))

Тож запитайте себе - якби хтось змінив реалізацію на щось неправильне, чи все-таки пройдуть ваші тести чи вони сказали б "зачекай на хвилину!"?

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


Як вказувала nanda, ви завжди можете додати нескінченну серію case тверджень до a switch, і ви не можете написати тест для кожного можливого введення та виведення для прикладу ОП.
Роберт Харві

Ви могли б технічно перевірити значення від Int64.MinValueдоInt64.MaxValue . Виконання зайняло б багато часу, але це чітко визначило б вимогу, не маючи помилок. З нинішньою технологією це неможливо (я підозрюю, що це може стати більш поширеним у майбутньому). на практиці), але теоретичний.
Ніхто

@rmx: Якщо ви могли це зробити, то тести були б алгоритмом, і вам більше не потрібно було б писати алгоритм.
Роберт Харві

Це правда. Моя університетська дисертація насправді передбачає автоматичне генерування впровадження, використовуючи одиничні тести як керівництво з генетичним алгоритмом як допоміжним засобом для TDD - і це можливо лише з твердими тестами. Різниця полягає в тому, що прив'язування ваших вимог до вашого коду, як правило, набагато складніше для читання та розуміння, ніж єдиний метод, що втілює одиничні тести. Тоді виникає питання: якщо ваша реалізація є проявом ваших одиничних тестів, а ваші одиничні тести - прояв ваших вимог, чому б просто не пропустити тестування взагалі? У мене немає відповіді.
Ніхто

Крім того, чи не ми, як люди, так само ймовірні, що помилимося в одиничних тестах, як ми в коді реалізації? То чому взагалі тестовий пристрій?
Ніхто

3

Просто напишіть більше тестів. В кінці кінців, було б коротше писати

public long CalculateFactorial(long input)
{
    return input <= 1 ? 1 : CalculateFactorial(input-1)*input;
}

ніж

public long CalculateFactorial(long input)
{
    switch (input) {
       case 0: return 1;
       case 1: return 1;
       case 2: return 2;
       case 3: return 6;
       case 4: return 24;
       case 5: return 120;
    }
}

:-)


3
Чому не просто правильно написати алгоритм?
Роберт Харві

3
@Robert, це правильний алгоритм для обчислення Факторіал числа від 0 до 5. Крім того, що робить «правильно» означає? Це дуже простий приклад, але коли він стає складнішим, стає багато градацій того, що означає "правильний". Чи достатньо правильною є програма, яка вимагає кореневого доступу? Чи правильне використання XML замість CSV? Ви не можете відповісти на це. Будь-який алгоритм правильний, якщо він задовольняє деяким бізнес-вимогам, які формулюються як тести в TDD.
П Швед

3
Слід зазначити, що оскільки тип виходу довгий, існує лише невелика кількість вхідних значень (20 або близько того), що функція, можливо, може правильно керувати, тому велике твердження комутатора не обов'язково є найгіршим виконанням - якщо швидкість більше Важливо, ніж розмір коду, оператор перемикання може бути способом, залежно від ваших пріоритетів.
користувач281377

3

Написання тестів "обману" добре, для досить малих значень "ОК". Але пам’ятайте - тестування одиниць завершено лише тоді, коли всі тести пройдуть і не можуть бути записані нові тести, які не зможуть . Якщо ви дійсно хочете мати метод CalculateFactorial, який містить купу висловлювань if (а ще краще, великий перемикач / випадок case :-), ви можете це зробити, і оскільки ви маєте справу з номером з фіксованою точністю, необхідний код реалізовувати це є кінцевим (хоча, ймовірно, досить великим і некрасивим, можливо, обмеженим компілятором чи системними обмеженнями щодо максимального розміру коду процедури). На даний момент, якщо ви дійснонаполягайте на тому, що вся розробка повинна керуватися одиничним тестом, ви можете написати тест, який вимагає коду для обчислення результату за коротший час, ніж той, який можна виконати, дотримуючись всіх гілок оператора if .

В основному, TDD може допомогти вам написати код, який реалізує вимоги правильно , але він не може змусити вас писати хороший код. Це залежить від вас.

Діліться та насолоджуйтесь.


+1 для "одиничне тестування закінчено лише тоді, коли всі тести пройдуть і не можуть бути написані нові тести, які не зможуть" Багато людей говорять, що законно повернути константу, але не слідкуйте за цим "на короткий термін" або " якщо загальні вимоги потребують лише тих конкретних випадків "
Тимін

1

Я на 100% згоден із пропозицією Роберта Харві, тут справа не лише в тому, щоб пройти тести, потрібно пам’ятати і про загальну мету.

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

Для Факторіалів тест виглядатиме так:

    [Theory]
    [InlineData(0, 1)]
    [InlineData( 1, 1 )]
    [InlineData( 2, 2 )]
    [InlineData( 3, 6 )]
    [InlineData( 4, 24 )]
    public void Test_Factorial(int input, int expected)
    {
        int result = Factorial( input );
        Assert.Equal( result, expected);
    }

Можна навіть реалізувати тестові дані (що повертаються IEnumerable<Tuple<xxx>>) та кодувати математичний інваріант, такий як багаторазове ділення на n дасть n-1).

Я вважаю, що цей tp є дуже потужним способом тестування.


1

Якщо ви все-таки зможете обдурити, то тестів недостатньо. Пишіть більше тестів! Для вашого прикладу я спробую додати тести з введенням 1, -1, -1000, 0, 10, 200.

Тим не менш, якщо ви дійсно зобов’язалися обманювати, ви можете написати нескінченний тоді. У цьому випадку нічого не могло допомогти, окрім перегляду коду. Ви незабаром потрапите на тест на прийняття ( написаний іншою людиною! )

Проблема з тестовими одиницями полягає в тому, що програмісти іноді розглядають їх як непотрібну роботу. Правильний спосіб їх бачити - це інструмент для того, щоб зробити результат своєї роботи правильним. Отже, якщо ви створюєте "if-then", ви несвідомо знаєте, що є інші випадки, які слід розглянути. Це означає, що вам доведеться написати ще тести. І так далі, і так далі, поки ви не зрозумієте, що обман не працює, і краще просто зашифрувати правильний спосіб. Якщо ти все ще відчуваєш, що не закінчиш, ти не закінчиш.


1
Тож здається, що ви говорите, що просто написати достатньо просто коду для проходження тесту (як захисники TDD) недостатньо. Ви також повинні пам’ятати про принципи продуманого програмного забезпечення. Я згоден з вами BTW.
Роберт Харві

0

Я б припустив, що ваш вибір тесту - не найкращий тест.

Я б почав із:

як перший тест, факториал (1),

факторіал (0) як другий

факторіал (-ве) як третій

а потім продовжуйте нерівіальні справи

і закінчити справу з переливом.


Що таке -ve??
Роберт Харві

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