В TDD, если я напишу тестовый пример, который проходит без изменения производственного кода, что это значит?

17

Вот правила Роберта С. Мартина для TDD :

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

Когда я пишу тест, который кажется стоящим, но проходит без изменения производственного кода:

  1. Значит ли это, что я сделал что-то не так?
  2. Должен ли я избегать написания таких тестов в будущем, если это поможет?
  3. Должен ли я оставить этот тест там или удалить его?

Примечание: я пытался задать этот вопрос здесь: Могу ли я начать с прохождения модульного теста? Но я не мог сформулировать вопрос достаточно хорошо до сих пор.

Даниэль Каплан
источник
«Ката для игры в боулинг», на которую вы ссылаетесь в статье, которую вы цитируете, на самом деле является проходным тестом.
ОАО

Ответы:

21

В нем говорится, что вы не можете написать производственный код, если только не пройдете неудачный модульный тест, а не то, что вы не можете написать тест, который проходит с самого начала. Цель правила - сказать: «Если вам нужно отредактировать производственный код, убедитесь, что вы сначала написали или изменили тест для него».

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

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

прецизионный самописец
источник
Улучшение охвата тестами существующего кода является еще одной совершенно веской причиной для написания (надеюсь) прохождения теста.
Джек,
13

Это означает, что либо:

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

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

public void TestAddMethod()
{
    Assert.IsTrue(Add(2,3) == 5);
}

Потому что все, что вам действительно нужно, это результат сложения 2 и 3.

Ваш метод реализации будет:

public int add(int x, int y)
{
    return x + y;
}

Но скажем, теперь мне нужно добавить 4 и 6 вместе:

public void TestAddMethod2()
{
    Assert.IsTrue(Add(4,6) == 10);
}

Мне не нужно переписывать мой метод, потому что он уже охватывает второй случай.

Теперь предположим, что я обнаружил, что моей функции Add действительно нужно возвращать число с некоторым потолком, скажем, 100. Я могу написать новый метод, который проверяет это:

public void TestAddMethod3()
{
    Assert.IsTrue(Add(100,100) == 100);
}

И этот тест сейчас провалится. Теперь я должен переписать свою функцию

public int add(int x, int y)
{
    var a = x + y;
    return a > 100 ? 100 : a;
}

чтобы это прошло.

Здравый смысл подсказывает, что если

public void TestAddMethod2()
{
    Assert.IsTrue(Add(4,6) == 10);
}

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

Роберт Харви
источник
5
Если вы полностью следовали примерам Мартина (и он не обязательно предлагает вам это сделать), чтобы add(2,3)пройти, вы буквально вернули бы 5. Жестко закодировано. Затем вы бы написали тест, для add(4,6)которого вы заставили бы вас написать производственный код, который проходит его, не прерываясь add(2,3)в то же время. Вы бы в конечном итоге с return x + y, но вы бы не начать с ним. Теоретически. Естественно, Мартин (или, может быть, это был кто-то другой, я не помню) любит приводить такие примеры для образования, но не ожидает, что вы на самом деле будете писать такой тривиальный код таким образом.
Энтони Пеграм
1
@tieTYT, как правило, если я правильно помню из книги (книг) Мартина, второго тестового примера, как правило, будет достаточно, чтобы вы могли написать общее решение для простого метода (и, на самом деле, вы бы действительно заставили его работать первый раз). Нет необходимости в третьем.
Энтони Пеграм
2
@tieTYT, тогда ты будешь писать тесты, пока не сделаешь. :)
Энтони Пеграм
4
Есть третья возможность, и она идет вразрез с вашим примером: вы написали дубликат теста. Если вы следите за TDD «неукоснительно», то новый пройденный тест всегда будет красным. После DRY вы никогда не должны писать два теста, которые проверяют, по сути, одно и то же.
congusbongus
1
«Если вы полностью следовали примерам Мартина (и он не обязательно предлагает вам это сделать), чтобы выполнить add (2,3), вы буквально вернули бы 5. Жестко закодировано». - это кусочек строгого TDD, который всегда был со мной, идея, что вы пишете код, который, как вы знаете, неверен в ожидании грядущего теста и его доказательства. Что, если этот будущий тест по какой-то причине никогда не будет написан, и коллеги предполагают, что «все тесты зеленые» подразумевают «все коды правильны»?
Джулия Хейворд
2

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

Допустим, канонический (?) TDD. Нет никакого производственного кода, но есть несколько тестовых случаев (которые, конечно, всегда терпят неудачу). Мы добавляем производственный код для передачи. Тогда остановитесь здесь, чтобы добавить больше неудачных тестов. Снова добавьте производственный код для передачи.

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

Мне лично не нравятся такие тоталитарные, бесчеловечные правила; (

9dan
источник
2

На самом деле та же проблема возникла в додзё прошлой ночью.

Я сделал быстрое исследование этого. Вот что я придумал:

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

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

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

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

leifbattermann
источник
0

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

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

Мы можем представить себе две крайности: один программист, который пишет большое количество тестов «на всякий случай», другой ловит ошибку; и второй программист, который тщательно анализирует проблемное пространство, прежде чем писать минимальное количество тестов. Допустим, оба пытаются реализовать функцию абсолютного значения.

Первый программист пишет:

assert abs(-88888) == 88888
assert abs(-12345) == 12345
assert abs(-5000) == 5000
assert abs(-32) == 32
assert abs(46) == 46
assert abs(50) == 50
assert abs(5001) == 5001
assert abs(999999) == 999999
...

Второй программист пишет:

assert abs(-1) == 1
assert abs(0) == 0
assert abs(1) == 1

Первая реализация программиста может привести к:

def abs(n):
    if n < 0:
        return -n
    elif n > 0:
        return n

Реализация второго программиста может привести к:

def abs(n):
    if n < 0:
        return -n
    else:
        return n

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

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

thinkterry
источник
Ну, второй программист был явно небрежен с тестами, потому что его коллега пересмотрел abs(n) = n*nи прошел.
Эйко,
@Eiko Ты абсолютно прав. Написание слишком небольшого количества тестов может вас так же сильно укусить. Второй программист был слишком скуп, хотя бы не тестировал abs(-2). Как и все, модерация является ключом.
Thinkterry