Как вы должны TDD игра Yahtzee?

36

Допустим, вы пишете в стиле ЯХДЗИ в стиле TDD. Вы хотите проверить часть кода, которая определяет, является ли набор из пяти бросков кубика фулл-хаусом. Насколько я знаю, при выполнении 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. В одном из эпизодов он создает стиль String 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;
}

После каждого шага Рой говорит: «Напиши самую простую вещь, которая будет работать».

Поэтому я подумал, что попробую этот подход, когда попытаюсь сделать игру Yahtzee в стиле TDD.

Кристоф Клас
источник
8
«Напишите, что самое простое, что работает» - это аббревиатура; правильный совет: «Напишите самую простую из возможных вещей, которая не совсем бессмысленная и, очевидно, неправильная, которая работает». Так что, нет, вы не должны писатьif (roll1 == 1 && roll2 == 1 && roll3 == 1 && roll4 == 2 && roll5 == 2)
Carson63000
3
Спасибо, что суммировали ответ Эрика, будь то менее аргументированным или цивилизованным способом.
Кристоф Клас
1
«Напишите самую простую вещь, которая работает», например, @ Carson63000, на самом деле является упрощением. На самом деле опасно так думать; это приводит к позорному разгрому Судоку TDD (Google это). При слепом следовании TDD действительно является мозговой смертью: вы не можете обобщить нетривиальный алгоритм, слепо выполняя «простейшую вещь, которая работает» ... вы должны думать! К сожалению, даже предполагаемые мастера XP и TDD иногда следуют ему вслепую ...
Андрес Ф.
1
@AndresF. Обратите внимание, что ваш комментарий оказался выше в поиске Google, чем большая часть комментариев о «разгроме Soduko TDD» менее чем через три дня. Тем не менее, как не решить судоку, подытожил: TDD за качество, а не за правильность. Вам нужно решить алгоритм перед началом кодирования, особенно с TDD. (Не то чтобы я тоже не первый программист кода).
Марк Херд
1
pvv.org/~oma/TDDinC_Yahtzee_27oct2011.pdf может представлять интерес.

Ответы:

40

На этот вопрос уже есть много хороших ответов, и я прокомментировал и проголосовал за некоторые из них. Тем не менее, я хотел бы добавить некоторые мысли.

Гибкость не для новичков

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

  • строгое соблюдение преподаваемых правил или планов
  • не применять дискреционные суждения

Это не описание недостатка личности, поэтому нет причин стыдиться этого - это этап, который нам всем нужно пройти, чтобы узнать что-то новое.

Это также верно для TDD.

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

Если новичок принимает совет о том, что иногда можно не использовать TDD, как он может определить, когда можно пропустить TDD?

Без опыта или руководства только новичок может пропустить TDD каждый раз, когда он становится слишком трудным. Это человеческая природа, но не хороший способ учиться.

Слушай тесты

Отказ от TDD в любое время, когда становится трудно, - упустить одно из самых важных преимуществ TDD. Тесты обеспечивают раннюю обратную связь об API SUT. Если тест трудно написать, это важный признак того, что SUT сложно использовать.

Вот почему одним из наиболее важных сообщений ГСНО является: слушайте свои тесты!

В случае с этим вопросом моей первой реакцией, когда я увидел предлагаемый API игры Yahtzee и обсуждение комбинаторики, которое можно найти на этой странице, было то, что это важный отзыв об API.

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

Тем не менее, я думаю, что некоторые комментарии к этому ответу ошибаются. Затем TDD предлагает следующее: как только вы поймете, что Rollкласс будет хорошей идеей, вы приостанавливаете работу над исходным SUT и начинаете работу над TDD Roll.

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

Затем, когда Rollкласс будет достаточно развит, вернитесь ли вы к исходному SUT и уточните его с точки зрения Rollвходных данных.

Предложение помощника по тестированию не обязательно подразумевает случайность - это просто способ сделать тест более читабельным.

Другим способом подхода и моделирования входных данных с точки зрения Rollэкземпляров было бы введение в построитель тестовых данных .

Красный / Зеленый / Рефакторинг - это трехступенчатый процесс

Хотя я согласен с общим мнением, что (если вы достаточно опытны в TDD), вам не нужно строго придерживаться TDD, я думаю, что это довольно плохой совет в случае с упражнением Yahtzee. Хотя я не знаю деталей правил Яхтзе, я не вижу здесь убедительного аргумента о том, что вы не можете строго придерживаться процесса «Красный / Зеленый / Рефакторинг» и все же достичь надлежащего результата.

Кажется, что большинство людей здесь забывают, это третья стадия процесса Red / Green / Refactor. Сначала вы пишете тест. Затем вы пишете простейшую реализацию, которая проходит все тесты. Тогда вы рефакторинг.

Именно здесь, в этом третьем штате, вы можете использовать все свои профессиональные навыки. Здесь вы можете размышлять над кодом.

Тем не менее, я думаю, что отговорка состоит в том, чтобы заявить, что вы должны только «написать простейшую вещь, которая не является полностью мозговой и явно неправильной, которая работает». Если вы (думаете, что) достаточно знаете о реализации заранее, то все, кроме полного решения, будет явно неверным . Что касается советов, то это довольно бесполезно для новичка.

Что в действительности должно произойти, так это то, что если вы можете выполнить все тесты с явно некорректной реализацией, то вам следует написать еще один тест .

Удивительно, как часто это приводит к совершенно иной реализации, нежели та, которую вы имели в виду вначале. Иногда такая альтернатива может оказаться лучше, чем ваш первоначальный план.

Rigor - это инструмент обучения

Имеет смысл придерживаться строгих процессов, таких как Red / Green / Refactor, пока вы учитесь. Это заставляет ученика приобретать опыт работы с TDD не только тогда, когда это легко, но и когда это сложно.

Только после того, как вы освоите все сложные части, вы сможете принять осознанное решение о том, когда следует отклониться от «истинного» пути. Вот когда вы начинаете формировать свой собственный путь.

Марк Симанн
источник
'Другой новичок TDD здесь, со всеми обычными опасениями по поводу попытки этого. Интересно, если вы сможете выполнить все тесты с явно некорректной реализацией, то вы должны написать еще один тест. Похоже, это хороший способ справиться с восприятием того, что тестирование реализаций «мозговых мертвецов» - ненужная занятая работа.
shambulator
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 частей на самом деле довольно сложно. Обычно будет использоваться литералы во время этапа с красного на зеленый, а затем абстрагировать их до констант на этапе рефакторинга или обобщить их в более позднем тесте.

Во время моего собственного путешествия с TDD я осознал, что необходимо провести важное различие - нехорошо смешивать «простое» и «тупое». То есть, когда я начинал, я смотрел, как люди делают TDD, и я думал, что «они просто делают самую глупую вещь, чтобы сделать тесты успешными», и я подражал этому некоторое время, пока не понял, что «простое» немного отличается чем "тупой". Иногда они перекрываются, но часто нет.

Таким образом, извинения, если у меня создалось впечатление, что существование литералов было проблемой - это не так. Я бы сказал, что сложность условного с 5 пунктами является проблемой. Ваш первый «красный-зеленый» может быть просто «вернуть истину», потому что это действительно просто (и, по совпадению, тупо). Следующий тестовый пример с (1, 2, 3, 4, 5) должен будет вернуть false, и именно здесь вы начнете оставлять «тупые» позади. Вы должны спросить себя: «Почему (1, 1, 1, 2, 2) аншлаг, а (1, 2, 3, 4, 5) нет?» Самая простая вещь, которую вы могли бы придумать, это то, что у одного есть последний элемент последовательности 5 или второй элемент последовательности 2, а у другого его нет. Это просто, но они также (без необходимости) тупые. Что вы действительно хотите, чтобы ездить на это "сколько из того же числа у них есть?" Таким образом, вы можете пройти второй тест, проверив, есть ли повторение. В одном с повтором у вас есть фулл-хаус, а в другом - нет. Теперь тест пройден, и вы пишете еще один тест, который повторяется, но не является полным аншлагом для дальнейшего совершенствования вашего алгоритма.

Вы можете или не можете сделать это с литералами, как вы идете, и это нормально, если вы делаете. Но общая идея заключается в том, что ваш алгоритм «органично» растет по мере добавления новых случаев.

Эрик Дитрих
источник
Я обновил свой вопрос, чтобы добавить больше информации о том, почему я начал с буквального подхода.
Кристоф Клас
9
Это отличный ответ.
Tallseth
1
Большое спасибо за ваш вдумчивый и хорошо объясненный ответ. Это действительно имеет большой смысл сейчас, когда я думаю об этом.
Кристоф Клас
1
Тщательное тестирование не означает тестирование каждой комбинации ... Это глупо. Для этого конкретного случая возьмите конкретный аншлаг или два и пару не фулл-хаусов. Также любые специальные комбинации, которые могут вызвать проблемы (например, 5 в своем роде).
Schleis
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);
}

После этого, держу пари, вам больше не нужно писать (может быть, две пары, или, может быть, yahtzee, если вы думаете, что это не фулл-хаус).

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

У этого подхода есть несколько преимуществ:

  • Вам не нужно писать тест, единственная цель которого - не дать вам застрять на определенных значениях.
  • Тесты очень хорошо сообщают о ваших намерениях (код первого теста кричит «любой фулл-хаус возвращает истину»)
  • это быстро доставит вас к сути проблемы
  • иногда он заметит случаи, о которых вы не думали
tallseth
источник
Если вы используете этот подход, вам нужно будет улучшить ваши сообщения журнала в ваших утверждениях Assert.That. Разработчик должен увидеть, какой вход вызвал сбой.
Bringer128
Разве это не создает дилемму курица или яйцо? Когда вы реализуете AnyFullHouse (также используя TDD), вам не понадобится IsFullHouse для проверки его правильности? В частности, если AnyFullHouse имеет ошибку, эта ошибка может быть воспроизведена в IsFullHouse.
свингинг
AnyFullHouse () - это метод в тестовом примере. Вы обычно TDD ваши тесты? Нет. Кроме того, гораздо проще создать случайный образец фулл-хауса (или любого другого броска), чем проверять его существование. Конечно, если в вашем тесте есть ошибка, она может быть воспроизведена в рабочем коде. Это верно для каждого теста, хотя.
13
AnyFullHouse - это вспомогательный метод в тестовом примере. Если они достаточно общие, то вспомогательные методы тоже проходят тестирование!
Марк Херд
Должен ли IsFullHouseдействительно вернуться, trueесли pairNum == trioNum ?
recursion.ninja
2

Я могу думать о двух основных способах, которые я бы рассмотрел при тестировании этого;

  1. Добавьте «несколько» больше тестовых случаев (~ 5) действительных наборов фулл-хауса, и такое же количество ожидаемых ложных срабатываний ({1, 1, 2, 3, 3} является хорошим. Помните, что, например, может быть 5 из них распознается как «3 одинаковых плюс пара» неправильной реализацией). Этот метод предполагает, что разработчик не просто пытается пройти тесты, но на самом деле правильно его реализовать.

  2. Протестируйте все возможные наборы кубиков (всего 252 разных). Это, конечно, предполагает, что у вас есть какой-то способ узнать ожидаемый ответ (при тестировании это называется oracle.) Это может быть эталонная реализация той же функции или человека. Если вы хотите быть очень строгим, возможно, стоит вручную написать каждый ожидаемый результат.

Так получилось, что однажды я написал AI Яхтзе, который, конечно, должен был знать правила. Вы можете найти код для части оценки счета здесь , пожалуйста, обратите внимание, что реализация предназначена для скандинавской версии (Yatzy), и наша реализация предполагает, что кости даны в отсортированном порядке.

ansjob
источник
Вопрос на миллион долларов: вы вывели Yahtzee AI, используя чистый TDD? Я держу пари, что ты не можешь; Вы должны использовать знание предметной области, которое по определению не является слепым :)
Andres F.
Да, я думаю, ты прав. Это общая проблема с TDD, когда тестовые примеры нуждаются в ожидаемых выходных данных, если только вы не хотите проверять только непредвиденные сбои и необработанные исключения.
Ответ
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;

И если вы получаете ввод, который не является действительным броском Яхтзе, вы должны бросить, как будто завтра нет.

Джей Мюллер
источник