Как исправить ошибку в тесте, после написания реализации

21

Каков наилучший способ действий в TDD, если после правильной реализации логики тест по-прежнему не проходит (потому что в тесте есть ошибка)?

Например, предположим, что вы хотели бы разработать следующую функцию:

int add(int a, int b) {
    return a + b;
}

Предположим, мы разработали его в следующие шаги:

  1. Написать тест (пока нет функции):

    // test1
    Assert.assertEquals(5, add(2, 3));
    

    Приводит к ошибке компиляции.

  2. Напишите фиктивную реализацию функции:

    int add(int a, int b) {
        return 5;
    }
    

    Результат: test1проходит.

  3. Добавьте еще один тест:

    // test2 -- notice the wrong expected value (should be 11)!
    Assert.assertEquals(12, add(5, 6));
    

    Результат: test2не удается, test1все еще проходит.

  4. Напишите реальную реализацию:

    int add(int a, int b) {
        return a + b;
    }
    

    Результат: test1все еще проходит, test2все еще терпит неудачу (так как 11 != 12).

В данном конкретном случае: было бы лучше:

  1. исправить test2и посмотреть, что сейчас проходит, или
  2. удалите новую часть реализации (т. е. вернитесь к шагу № 2, приведенному выше), исправьте test2и оставьте ее неудачной, а затем снова введите правильную реализацию (шаг № 4 выше).

Или есть какой-то другой, более умный способ?

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

РЕДАКТИРОВАТЬ (в ответ на ответ @Thomas Junk):

Основное внимание в этом вопросе уделяется тому, что TDD предлагает в таком случае, а не тому, что является «универсальной передовой практикой» для достижения хорошего кода или тестов (которые могут отличаться от способа TDD).

Аттило
источник
3
Рефакторинг на красной полосе является актуальной концепцией.
RubberDuck
5
Ясно, что вы должны делать TDD на вашем TDD.
Blrfl
17
Если кто-нибудь спросит меня, почему я скептически отношусь к TDD, я укажу им на этот вопрос. Это Кафка.
Traubenfuchs
@Blrfl, это то, что нам говорит Xibit »Я поместил TDD в TDD, чтобы вы могли TDD во время TDDing«: D
Томас Джанк,
3
@Traubenfuchs Я признаю, что на первый взгляд вопрос кажется глупым, и я не сторонник «делай TDD все время», но я верю, что есть большое преимущество, чтобы увидеть провал теста, а затем написать код, который делает тест успешным (о чем этот вопрос на самом деле, в конце концов).
Винсент

Ответы:

31

Абсолютно критическим моментом является то, что вы видите, что тест прошел и не прошел

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

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


Рефакторинг против красной полосы дает нам формальные шаги для рефакторинга рабочего теста:

  • Запустить тест
    • Обратите внимание на зеленую полосу
    • Сломать проверяемый код
  • Запустить тест
    • Обратите внимание на красную полосу
    • Рефакторинг теста
  • Запустить тест
    • Обратите внимание на красную полосу
    • Разбить проверяемый код
  • Запустить тест
    • Обратите внимание на зеленую полосу

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

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

Здесь также выполняется откат кода, но этого достаточно, чтобы тест не прошел. Если этого недостаточно для охвата всего кода, введенного в то время, когда он охватывается только тестом с ошибками, нам требуется больший откат кода и больше тестов.

Введите зеленый тест

  • Запустить тест
    • Обратите внимание на зеленую полосу
    • Сломать проверяемый код
  • Запустить тест
    • Обратите внимание на красную полосу
    • Разбить проверяемый код
  • Запустить тест
    • Обратите внимание на зеленую полосу

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

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

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

Мой благодаря Rubberduck для Охватывая Красной Bar ссылки.

candied_orange
источник
2
Мне больше нравится этот ответ: важно, чтобы тест не прошел с неправильным кодом, поэтому я хотел бы удалить / закомментировать код, исправить тесты и увидеть, как они провалились, вернуть код (возможно, ввести преднамеренную ошибку, чтобы поставить тесты на тест) и исправьте код, чтобы он заработал. Это очень XP, чтобы полностью удалить и переписать его, но иногда вам просто нужно быть прагматичным. ;)
GolezTrol
@GolezTrol Я думаю, что мой ответ говорит о том же, поэтому я был бы признателен за любые ваши отзывы о том, было ли это неясно.
Джоншарпе
@jonrsharpe Твой ответ тоже хорош, и я проголосовал за него еще до того, как прочел этот. Но если вы очень строги в обращении с кодом, CandiedOrange предлагает более прагматичный подход, который мне больше нравится.
GolezTrol
@GolezTrol Я не сказал, как вернуть код; закомментируйте, вырежьте и вставьте, спрячьте, используйте историю вашей IDE; это на самом деле не имеет значения. Главное, почему вы делаете это: чтобы вы могли убедиться, что вы получаете правильный сбой. Я редактировал, надеюсь, уточнить.
Джоншарпе
10

Какую общую цель вы хотите достичь?

  • Делать хорошие тесты?

  • Делаете правильную реализацию?

  • Делать TTD религиозно правильно ?

  • Ни один из вышеперечисленных?

Возможно, вы переосмыслите свое отношение к тестам и тестированию.

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

Принимая ваш пример:

«Правильная» реализация дополнения будет эквивалентна коду a+b. И пока ваш код делает это, вы бы сказали, что алгоритм верен в том, что он делает, и он правильно реализован.

int add(int a, int b) {
    return a + b;
}

На первый взгляд , мы оба согласились бы, что это является реализация дополнения.

Но то, что мы делаем, на самом деле не говорит о том, что этот код является его реализацией, additionон ведет себя только в определенной степени как единое целое : подумайте о целочисленном переполнении .

Целочисленное переполнение происходит в коде, но не в концепции addition. Итак: ваш код ведет себя в определенной степени как концепция addition, но это не так addition.

Эта довольно философская точка зрения имеет несколько последствий.

И можно сказать, что тесты - это не более чем предположения об ожидаемом поведении вашего кода. При тестировании кода, вы могли бы (возможно) никогда не убедитесь , что ваша реализация является право , то лучшее , что вы могли бы сказать, что ваши ожидания относительно того , что результаты ваш код поставляет были или не были удовлетворены; будь то, что ваш код неправильный, будь то, что ваш тест не так или будь то, что оба они не правы.

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

Это не помогает, когда вы сделали неправильные предположения; но эй! по крайней мере, это предотвращает шизофрению: ожидание других результатов, когда их не должно быть.


ТЛ; др

Каков наилучший способ действий в TDD, если после правильной реализации логики тест по-прежнему не проходит (потому что в тесте есть ошибка)?

Ваши тесты - это предположения о поведении кода. Если у вас есть веские основания полагать, что ваша реализация верна, исправьте тест и посмотрите, выполняется ли это предположение.

Томас Джанк
источник
1
Я думаю, что вопрос об общих целях очень важен, спасибо, что подняли его. Для меня наивысшим приоритетом является следующее: 1. правильная реализация 2. «хорошие» тесты (или, я бы сказал, «полезные» / «хорошо разработанные» тесты). Я вижу TDD как возможный инструмент для достижения этих двух целей. Так что, хотя я не хочу обязательно религиозно следовать TDD, в контексте этого вопроса меня больше всего интересует перспектива TDD. Я отредактирую вопрос, чтобы уточнить это.
Аттилио
Итак, вы бы написали тест, который проверяет переполнение и проходит, когда это происходит, или вы заставляете его не работать, когда это происходит, потому что алгоритм является сложением, а переполнение дает неправильный ответ?
Джерри Иеремия
1
@JerryJeremiah Моя точка зрения такова: то, что ваши тесты должны охватывать, зависит от вашего варианта использования. Для случая использования, когда вы складываете несколько цифр, алгоритм достаточно хорош . Если вы знаете, что очень вероятно, что вы сложите «большие числа», то datatypeэто явно неправильный выбор. Тест показал бы, что: ваше ожидание будет «работает для больших чисел» и в некоторых случаях не будет выполнено. Тогда вопрос будет в том, как бороться с этими случаями. Это угловые дела? Когда да, как с ними бороться? Возможно, некоторые оговорки, связанные с квардом, помогают предотвратить беспорядок. Ответ связан с контекстом.
Томас Джанк
7

Вы должны знать, что тест будет неуспешным, если реализация ошибочна, что не то же самое, что проходить, если реализация верна. Поэтому вы должны вернуть код в состояние, в котором вы ожидаете, что он потерпит неудачу, прежде чем исправлять тест, и убедиться, что он потерпел неудачу по той причине, которую вы ожидали (т.е. 5 != 12), а не что-то еще, что вы не предсказывали.

jonrsharpe
источник
Как мы можем проверить, что тест не пройден по той причине, на которую мы рассчитываем?
Basilevs
2
@Basilevs вы: 1. выдвигаете гипотезу о том, какой должна быть причина неудачи; 2. запустить тест; и 3. прочитать полученное сообщение об ошибке и сравнить. Иногда это также предлагает способы, которыми вы могли бы переписать тест, чтобы дать вам более значимую ошибку (например, assertTrue(5 == add(2, 3))дает менее полезный вывод, чем assertEqual(5, add(2, 3))хотя они оба тестируют одно и то же).
Джоншарпе
До сих пор неясно, как применить этот принцип здесь. У меня есть гипотеза - тест возвращает постоянное значение, как повторный тест подтвердит, что я прав? Очевидно, чтобы проверить это, мне нужен ДРУГОЙ тест. Предлагаю добавить явный пример для ответа.
Базилевс
1
@Basilevs что? Ваша гипотеза на шаге 3 будет такой: «тест не пройден, потому что 5 не равен 12» . Запуск теста покажет вам, прошел ли тест по этой причине, в каком случае вы продолжаете, или по какой-то другой причине, и в этом случае вы выясняете, почему. Возможно, это языковая проблема, но мне неясно, что вы предлагаете.
Джоншарпе
5

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

Однако та же проблема может возникнуть в более сложных ситуациях, например, когда у вас есть ошибка в вашем установочном коде. В этом случае, после исправления теста, вы, вероятно, должны попытаться изменить свою реализацию таким образом, чтобы этот конкретный тест не прошел, а затем отменить мутацию. Если отмена реализации является самым простым способом сделать это, то это нормально. В вашем примере, вы можете мутировать a + bв a + aили a * b.

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

Вон Катон
источник
0

Я бы сказал, это случай для вашей любимой системы контроля версий:

  1. Проведите исправление теста, сохранив изменения кода в рабочем каталоге.
    Подтвердить с соответствующим сообщением Fixed test ... to expect correct output.

    При gitэтом для этого может потребоваться использование, git add -pесли тест и реализация находятся в одном и том же файле, в противном случае вы, очевидно, можете просто разделить два файла по отдельности.

  2. Зафиксируйте код реализации.

  3. Вернитесь во времени, чтобы проверить фиксацию, сделанную на шаге 1, и убедитесь, что тест на самом деле провалился .

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

cmaster - восстановить монику
источник