Як слід TDD грати Yahtzee?


36

Скажімо, ви пишете стиль TDD для гри Yahtzee. Ви хочете перевірити частину коду, яка визначає, чи є набір з п'яти рулонів штампу - це повний дім. Наскільки мені відомо, виконуючи TDD, ви дотримуєтесь цих принципів:

  • Спочатку напишіть тести
  • Напишіть найпростішу можливу річ, яка працює
  • Уточнити і рефактор

Тож початковий тест може виглядати приблизно так:

public void Returns_true_when_roll_is_full_house()
{
    FullHouseTester sut = new FullHouseTester();
    var actual = sut.IsFullHouse(1, 1, 1, 2, 2);

    Assert.IsTrue(actual);
}

Дотримуючись "Написати найпростішу можливу річ, яка працює", тепер слід написати такий IsFullHouseспосіб:

public bool IsFullHouse(int roll1, int roll2, int roll3, int roll4, int roll5)
{
    if (roll1 == 1 && roll2 == 1 && roll3 == 1 && roll4 == 2 && roll5 == 2)
    {
        return true;
    }

    return false;
}

Це призводить до зеленого тесту, але реалізація є неповною.

Чи слід перевіряти кожну можливу комбіновану комбінацію (як значень, так і позицій) для повного будинку? Це виглядає як єдиний спосіб бути абсолютно впевненим, що ваш IsFullHouseкод повністю перевірений і правильний, але це також звучить досить божевільно.

Як би ви перевірили щось подібне?

Оновлення

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

Мій практичний досвід тестування одиниць (особливо з використанням TDD-підходу) дуже обмежений. Пам’ятаю, як дивився запис майстер-класу TDD Роя Ошерова на Tekpub. В одному з епізодів він будує стиль StD Calculator TDD. Повну специфікацію калькулятора струн можна знайти тут: http://osherove.com/tdd-kata-1/

Він починає з такого тесту:

public void Add_with_empty_string_should_return_zero()
{
    StringCalculator sut = new StringCalculator();
    int result = sut.Add("");

    Assert.AreEqual(0, result);
}

Це призводить до цієї першої реалізації Addспособу:

public int Add(string input)
{
    return 0;
}

Потім додається цей тест:

public void Add_with_one_number_string_should_return_number()
{
    StringCalculator sut = new StringCalculator();
    int result = sut.Add("1");

    Assert.AreEqual(1, result);
}

І Addметод реконструюється:

public int Add(string input)
{
    if (input.Length == 0)
    {
        return 0;
    }

    return 1;
}

Після кожного кроку Рой каже "Напишіть найпростішу річ, яка буде працювати".

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


8
"Напишіть найпростішу можливу річ, яка працює" - це насправді абревіатура; правильна порада: "Напишіть найпростішу з можливих речей, яка не є повністю розумною та, очевидно, неправильною, що працює". Отже, ні, не варто писатиif (roll1 == 1 && roll2 == 1 && roll3 == 1 && roll4 == 2 && roll5 == 2)
Carson63000

3
Дякую за узагальнення відповіді Еріка, будь то менш аргументованим чи цивілізованим.
Крістоф Клайс

1
"Напишіть найпростішу річ, яка працює", наприклад @ Carson63000, насправді є спрощенням. Насправді небезпечно так думати; це призводить до сумнозвісного дедуля Судоку TDD (google it). Якщо сліпо слідувати, TDD - це насправді розум: ти не можеш узагальнити нетривіальний алгоритм, сліпо виконуючи "найпростішу справу, що працює" ... ти мусиш насправді думати! На жаль, навіть передбачувані майстри XP та TDD іноді сліпо слідують за ним ...
Андрес Ф.

1
@AndresF. Зауважте, ваш коментар виявився вищим у пошукових запитах Google, ніж більша частина коментарів щодо "дедукта Содуко ТДД" менше ніж за три дні. Проте як не вирішити судоку, підсумував це: TDD - це якість, а не коректність. Ви повинні вирішити алгоритм перед початком кодування, особливо з TDD. (Не те, щоб я теж не перший програміст з кодом.)
Марк Херд

1
pvv.org/~oma/TDDinC_Yahtzee_27oct2011.pdf може представляти інтерес.

Відповіді:


40

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

Гнучкість не для початківців

В ОП чітко зазначено, що він не мав досвіду TDD, і я думаю, що хороша відповідь повинна враховувати це. У термінології моделі Дрейфуса набуття навичок він, мабуть, новачок . Немає нічого поганого в тому, щоб бути початківцем - ми всі початківці, коли починаємо вчитися чомусь новому. Однак те, що пояснює модель Дрейфуса, - це те, що новачки характеризуються

  • жорстке дотримання вчених правил чи планів
  • відсутність дискреційного судження

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

Це справедливо і для TDD.

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

Якщо новачок приймає пораду про те, що іноді нормально не робити TDD, як він може визначити, коли це добре, щоб пропустити TDD?

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

Прослухайте тести

Пропускати TDD будь-коли, коли стає важко, - це пропустити одну з найважливіших переваг TDD. Тести забезпечують ранній зворотній зв'язок про API SUT. Якщо тест важко написати, це важливий знак того, що SUT важко використовувати.

Саме тому одне з найважливіших повідомлень GOOS : слухайте свої тести!

У випадку з цим питанням моя перша реакція на перегляд запропонованого API гри Yahtzee та обговорення комбінаторики, яку можна знайти на цій сторінці, полягала в тому, що це важливий відгук про API.

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

Однак я думаю, що деякі коментарі до цієї відповіді помилково. Тоді, що TDD пропонує, це те, що як тільки ви Rollзрозумієте, що клас був би гарною ідеєю, ви призупиняєте роботу над оригінальним SUT і починаєте працювати над TDD'ing Rollкласом.

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

Потім, як тільки Rollклас розвинеться достатньо, ви повернетесь до оригінального SUT і розгорніть його з точки зору Rollвхідних даних.

Пропозиція Тест-помічника не обов'язково передбачає випадковість - це лише спосіб зробити тест більш читабельним.

Ще одним способом підходу та моделювання даних з точки зору Rollвипадків буде запровадження тестового збирача даних .

Червоний / зелений / рефактор - це триетапний процес

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

Що, здається, більшість людей тут забуває, це третій етап процесу Червоного / Зеленого / Рефактора. Спочатку ви пишете тест. Потім ви пишете найпростішу реалізацію, яка проходить усі тести. Тоді ви рефактор.

Саме тут, в цьому третьому стані, ви можете принести всі свої професійні навички. Тут вам дозволяється розмірковувати над кодом.

Однак, я думаю, що це копія, щоб стверджувати, що ви повинні лише "Написати найпростішу можливу річ, яка не є повністю мозковою головою і, очевидно, неправильна, що працює". Якщо ви (думаєте, що) достатньо знаєте про реалізацію заздалегідь, то все, що не стосується повного рішення, буде очевидно невірним . Що ж стосується порад, то для початківця це досить марно.

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

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

Ригор - це інструмент навчання

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

Тільки коли ви освоїли всі важкі частини, ви зможете прийняти зважене рішення про те, коли відхилитися від «справжнього» шляху. Саме тоді ви починаєте формувати свій власний шлях.


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

1
Вау дякую Мені дуже страшно тенденцією людей говорити новачкам в TDD (або будь-якій дисципліні) «не турбуйся про правила, просто роби те, що відчуваєш найкраще». Як ти можеш знати, що відчуваєш себе найкраще, коли ти не маєш ні знань, ні досвіду? Я також хотів би згадати принцип пріоритетності трансформації, або цей код повинен стати більш загальним, оскільки тести стають більш конкретними. більшість убогих прихильників TDD, як дядько Боб, не стоять за поняттям "просто додати нове твердження if для кожного тесту".
сара

41

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

Перш за все, найпростішим, що ви могли зробити, щоб ваш тест пройшов, було б це:

public bool IsFullHouse(int roll1, int roll2, int roll3, int roll4, int roll5)
{
    return true;
}

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

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

public void Returns_true_when_roll_is_full_house()
{
    FullHouseTester sut = new FullHouseTester();
    var actual = sut.IsFullHouse(1, 2, 3, 4, 5);

    Assert.IsFalse(actual);
}

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

public void Returns_true_when_roll_is_full_house()
{
    FullHouseTester sut = new FullHouseTester();
    var actual = sut.IsFullHouse(-1, -2, -3, -4, -5);

    //I dunno - throw exception, return false, etc, whatever you think it should do....
}

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

Оновлення:

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

Я б сказав, що проблема не в існуванні буквалів, періоду, а в тому, що «найпростіша» річ є умовною у 5 частинах. Коли ви задумаєтесь над тим, 5-частинна умовна насправді є досить складною. Буде прийнято використовувати літерали під час кроку червоно-зеленого кольору, а потім абстрагувати їх константами на етапі рефактора або іншим чином узагальнити їх у подальшому тесті.

Під час своєї власної подорожі з TDD я зрозумів, що слід зробити важливе розмежування - недобре плутати "просте" та "тупе". Тобто, коли я починав, я спостерігав, як люди роблять TDD, і думав, що "вони просто роблять найглумішу річ, щоб змусити пройти тести", і я це деякий час наслідував, поки не зрозумів, що "просто" тонко відрізняється. ніж «тупий». Іноді вони перетинаються, але часто ні.

Отже, вибачте, якщо я створив враження, що існування буквалів було проблемою - це не так. Я б сказав, що складність умовного з 5 пунктами є проблемою. Ваш перший червоно-зелений колір може бути просто "поверненням істини", тому що це справді просто (і тупо, за збігом обставин). Наступний тестовий випадок із (1, 2, 3, 4, 5) повинен буде повернути помилковий, і саме тут ви почнете залишати «тупі» позаду. Ви повинні запитати себе "чому (1, 1, 1, 2, 2) є повноцінним будинком і (1, 2, 3, 4, 5) не є?" Найпростіша річ, яку ви можете придумати, може полягати в тому, що один має останній елемент послідовності 5 або другий елемент послідовності 2, а другий - ні. Це просто, але вони також (без потреби) тупі. Те, що ви насправді хочете їхати, - це "скільки однакової кількості у них?" Таким чином, ви можете пройти другий тест, перевіривши, чи є повторення чи ні. В одному з повтором у вас повний будинок, а в другому - ні. Тепер тест проходить, і ви пишете ще один тестовий випадок, який має повторення, але не є повноцінним завданням для подальшого вдосконалення вашого алгоритму.

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


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

9
Це чудова відповідь.
tallseth

1
Дуже дякую за вашу продуману та добре пояснену відповідь. Це насправді має багато сенсу зараз, коли я думаю про це.
Крістоф Клайс

1
Ретельне тестування не означає тестування кожної комбінації ... Це нерозумно. Для цього конкретного випадку візьміть конкретний повний будинок або два та пару не повних будинків. Також будь-які спеціальні комбінації, які могли б спричинити неприємності (тобто 5 у своєму роді).
Шлейс

3
+1 Принципи, що стоять за цією відповіддю, описані Робертом К. Мартіном з питань пріоритетності трансформації Cleancoder.posterous.com/the-transformation-priority-premise
Марк

5

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

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


5

Відповідь Еріка чудова, але я подумав, що можу поділитися хитрістю у написанні тестів.

Почніть з цього тесту:

[Test]
public void FullHouseReturnsTrue()
{
    var pairNum = AnyDiceValue();
    var trioNum = AnyDiceValue();

    Assert.That(sut.IsFullHouse(trioNum, pairNum, trioNum, pairNum, trioNum));
}

Цей тест стає ще кращим, якщо ви створите Rollклас замість того, щоб здати 5 парам:

[Test]
public void FullHouseReturnsTrue()
{
    var roll = AnyFullHouse();

    Assert.That(sut.IsFullHouse(roll));
}

Це дає цю реалізацію:

public bool IsFullHouse(Roll toCheck)
{
    return true;
}

Потім напишіть цей тест:

[Test]
public void StraightReturnsFalse()
{
    var roll = AnyStraight();

    Assert.That(sut.IsFullHouse(roll), Is.False);
}

Коли це пройде, напишіть це:

[Test]
public void ThreeOfAKindReturnsFalse()
{
    var roll = AnyStraight();

    Assert.That(sut.IsFullHouse(roll), Is.False);
}

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

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

У цього підходу є кілька переваг:

  • Вам не потрібно писати тест, єдиною метою якого є унеможливити зациклювання на конкретних значеннях
  • Тести дуже добре передають ваш намір (код першого тесту кричить "будь-який повний будинок повертається істинним")
  • це швидко допоможе вам працювати над проблемою
  • іноді він помітить випадки, про які ви не думали

Якщо ви зробите такий підхід, вам потрібно буде покращити повідомлення журналу у ваших Assert.That твердженнях. Розробнику потрібно побачити, який вхід спричинив збій.
Bringer128

Чи це не створює дилеми з куркою чи яйцем? Коли ви реалізуєте AnyFullHouse (використовуючи також TDD), чи не знадобиться вам IsFullHouse для перевірки його правильності? Зокрема, якщо AnyFullHouse має помилку, ця помилка може бути реплікувана в IsFullHouse.
віск

AnyFullHouse () - метод у тестовому випадку. Ви зазвичай TDD ваші тестові справи? Ні. Також набагато простіше створити випадковий зразок повноцінного будинку (або будь-якого іншого рулону), ніж перевірити його на існування. Звичайно, якщо ваш тест має помилку, він може бути повторений у виробничому коді. Це правда для кожного тесту, хоча.
tallseth

AnyFullHouse - це "помічник" методу в тестовому випадку. Якщо вони є загальнодоступними допоміжними методами, тестуйте теж!
Марк Херд

Чи IsFullHouseсправді слід повернутися, trueякщо pairNum == trioNum ?
recursion.ninja

2

Я можу придумати два основні способи, які б я розглядав під час тестування цього;

  1. Додайте "кілька" більше тестових випадків (~ 5) дійсних повнофункціональних наборів, і стільки ж очікуваних помилок ({1, 1, 2, 3, 3} є хорошим. Пам’ятайте, що 5 таких, наприклад, можуть бути невірною реалізацією визнано "3 однакових плюс пари"). Цей метод передбачає, що розробник не просто намагається пройти тести, а фактично реалізує його правильно.

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

Як це відбувається, я один раз написав AI Yahtzee AI, який, звичайно, повинен був знати правила. Ви можете знайти код для оцінки результатів оцінки тут. Зверніть увагу, що реалізація призначена для скандинавської версії (Yatzy), і наша реалізація передбачає, що кістки надаються в упорядкованому порядку.


Питання на мільйон доларів полягає в тому, чи ви виводили AI Yahtzee за допомогою чистого TDD? Моя обставина, що ви не можете; ви повинні використовувати доменні знання, які за визначенням не сліпі :)
Андрес Ф.

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

0

Цей приклад справді пропускає суть. Тут ми говоримо про єдину функцію, а не про розробку програмного забезпечення. Це трохи складно? так, так ви розбиваєте його. І ви абсолютно не перевіряєте кожен можливий вхід від 1, 1, 1, 1, 1 до 6, 6, 6, 6, 6, 6. Ця функція не потребує порядку, а комбінації, а саме AAABB.

Вам не потрібно 200 окремих логічних тестів. Ви можете використовувати набір, наприклад. Практично в будь-якій мові програмування є одна вбудована:

Set set;
set.add(a);
set.add(b);
set.add(c);
set.add(d);
set.add(e);

if(set.size() == 2) { // means we *must* be of the form AAAAB or AAABB.
    if(a==b==c==d) // eliminate AAAAB
        return false;
    else
        return true;
}
return false;

І якщо ви отримаєте внесок, який не є дійсним рухом Yahtzee, ви повинні кинути, як завтра немає.

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