При выполнении 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;
}
}
Технически, это правильно, потому что это действительно минимальный код, необходимый для того, чтобы выполнить этот конкретный тестовый проход («зеленый»), хотя это явно «обман», так как он даже не пытается выполнять функцию вычисления факториала. Конечно, теперь часть рефакторинга становится упражнением в «написании правильной функциональности», а не настоящим рефакторингом реализации. Очевидно, что добавление дополнительных тестов с другими параметрами завершится неудачно и вызовет рефакторинг, но вы должны начать с этого одного теста.
Итак, мой вопрос: как вы получаете такой баланс между «написанием минимального кода для прохождения теста», сохраняя при этом его работоспособность и в духе того, чего вы на самом деле пытаетесь достичь?
источник
Ответы:
Это совершенно законно. Красный, Зеленый, Рефакторинг.
Первый тест проходит.
Добавьте второй тест с новым входом.
Теперь быстро доберитесь до зеленого, вы можете добавить if-else, который отлично работает. Это проходит, но вы еще не сделали.
Третья часть Red, Green, Refactor является наиболее важной. Рефакторинг для устранения дублирования . Теперь у вас будет дублирование в вашем коде. Два оператора, возвращающие целые числа. И единственный способ устранить это дублирование - правильно закодировать функцию.
Я не говорю, что не пишите это правильно с первого раза. Я просто говорю, что это не обман, если нет.
источник
Очевидно, что требуется понимание конечной цели и достижение алгоритма, который соответствует этой цели.
TDD не волшебная пуля для дизайна; вам все еще нужно знать, как решать проблемы с помощью кода, и вам все еще нужно знать, как это сделать на уровне выше нескольких строк кода, чтобы пройти тест.
Мне нравится идея TDD, потому что она поощряет хороший дизайн; это заставляет вас думать о том, как вы можете написать свой код, чтобы он был тестируемым, и в целом эта философия подтолкнет код к лучшему дизайну в целом. Но вы все равно должны знать, как разработать решение.
Я не одобряю редукционистские философии TDD, которые утверждают, что вы можете создать приложение, просто написав наименьшее количество кода для прохождения теста. Не думая об архитектуре, это не сработает, и ваш пример доказывает это.
Дядя Боб Мартин говорит это:
источник
Очень хороший вопрос ... и я должен не согласиться почти со всеми, кроме @Robert.
Письмо
Выполнение одного прохода теста для факториальной функции - пустая трата времени . Это не «обман» и не следование буквально красно-зеленому рефактору. Это неправильно .
Вот почему:
аргументы «рефактора» ошибочны; если у вас есть два случая испытания для 5 и 6, этот код все еще не так, потому что вы не вычисление факториала на все :
если мы следуем аргументу 'refactor' буквально , тогда, когда у нас есть 5 тестовых случаев, мы вызовем YAGNI и реализуем функцию, используя таблицу поиска:
Ни один из них на самом деле ничего не рассчитывает, вы . И это не задача!
источник
Когда вы написали только один модульный тест, однострочная реализация (
return 120;
) является допустимой. Написание цикла, вычисляющего значение 120 - это было бы обманом!Такие простые начальные тесты являются хорошим способом выявления крайних случаев и предотвращения разовых ошибок. Пять на самом деле не входное значение, с которого я бы начал.
Эмпирическое правило, которое может быть полезно здесь: ноль, один, много, много . Ноль и один являются важными крайними случаями для факториала. Их можно реализовать с помощью однострочников. Тестовый пример "много" (например, 5!) Заставит вас написать цикл. Тестовый набор "lot" (1000 !?) может заставить вас реализовать альтернативный алгоритм для обработки очень больших чисел.
источник
factorial(5)
на плохой первый тест. мы начинаем с простейших возможных случаев, и в каждой итерации мы делаем тесты немного более конкретными, призывая код стать немного более универсальным. это то, что дядя Боб называет предпосылкой приоритета трансформации ( blog.8thlight.com/uncle-bob/2013/05/27/… )Пока у вас есть только один тест, минимальный код, необходимый для прохождения теста, действительно
return 120;
, и вы можете легко сохранить его, если у вас больше нет тестов.Это позволяет вам отложить дальнейшее проектирование до тех пор, пока вы на самом деле не напишите тесты, которые выполняют ДРУГИЕ возвращаемые значения этого метода.
Пожалуйста, помните, что этот тест является работоспособной версией вашей спецификации, и если все, что говорится в этой спецификации, это то, что f (6) = 120, то это идеально соответствует требованиям.
источник
Если вы можете «обмануть» таким образом, это говорит о том, что ваши юнит-тесты некорректны.
Вместо того, чтобы тестировать метод факториала с одним значением, протестируйте его как диапазон значений. Здесь может помочь тестирование на основе данных.
Рассматривайте свои модульные тесты как проявление требований - они должны совместно определять поведение метода, который они тестируют. (Это называется поведенческим развитием - это будущее
;-)
)Так что спросите себя - если бы кто-то изменил реализацию на что-то неправильное, ваши тесты все равно пройдут или они скажут «подождите минутку!»?
Принимая это во внимание, если ваш единственный тест был тем, который был в вашем вопросе, то технически соответствующая реализация верна. Проблема тогда рассматривается как плохо определенные требования.
источник
case
операторов в aswitch
, и вы не можете написать тест для каждого возможного ввода и вывода для примера OP.Int64.MinValue
доInt64.MaxValue
. Выполнение этого заняло бы много времени, но оно явно определило бы требование без места для ошибки. С нынешней технологией это невыполнимо (я подозреваю, что это может стать более распространенным в будущем), и я согласен, что вы могли бы обмануть, но я думаю, что вопрос ОП не был практичным (никто не мог бы обмануть таким образом на практике), но теоретический.Просто напишите больше тестов. В конце концов, было бы короче, чтобы написать
чем
:-)
источник
Написание «читерских» тестов в порядке, для достаточно малых значений «ОК». Но помните - модульное тестирование завершается только тогда, когда все тесты пройдены, и новые тесты не могут быть написаны, если они не пройдут . Если вы действительно хотите иметь метод CalculateFactorial, который содержит кучу операторов if (или, что еще лучше, большой оператор switch / case :-), вы можете это сделать, и, поскольку вы имеете дело с числом с фиксированной точностью, необходим код реализовать это конечно (хотя, вероятно, довольно большой и некрасивый, и, возможно, ограниченный компилятором или системными ограничениями на максимальный размер кода процедуры). На данный момент, если вы действительнонастаивайте на том, что вся разработка должна вестись модульным тестом. Вы можете написать тест, который требует, чтобы код вычислял результат за промежуток времени, меньший, чем тот, который можно выполнить, следуя всем ветвям оператора if .
По сути, TDD может помочь вам написать код, который правильно реализует требования , но не может заставить вас писать хороший код. Это зависит от вас.
Поделитесь и наслаждайтесь.
источник
Я на 100% согласен с предложением Роберта Харвиза: речь идет не только о прохождении тестов, но и об общей цели.
В качестве решения вашей основной задачи: «Проверено только на работу с заданным набором входных данных», я бы предложил использовать управляемые данными тесты, такие как теория xunit. Сила этой концепции заключается в том, что она позволяет легко создавать спецификации входов к выходам.
Для Factorials тест будет выглядеть так:
Вы могли бы даже реализовать предоставление тестовых данных (которое возвращает
IEnumerable<Tuple<xxx>>
) и закодировать математический инвариант, такой как многократное деление на n приведет к n-1).Я считаю, что это очень мощный способ тестирования.
источник
Если вы все еще можете обмануть, то тестов недостаточно. Напишите больше тестов! Для вашего примера я постараюсь добавить тесты с вводом 1, -1, -1000, 0, 10, 200.
Тем не менее, если вы действительно привержены обману, вы можете написать бесконечное «если-тогда». В этом случае ничто не может помочь, кроме проверки кода. Вас скоро поймают на приемочном тесте ( написано другим человеком! )
Проблема модульных тестов заключается в том, что иногда программисты считают их ненужной работой. Правильный способ их увидеть - это инструмент для того, чтобы сделать результат своей работы правильным. Таким образом, если вы создаете if-then, вы неосознанно знаете, что есть другие случаи, которые нужно рассмотреть. Это означает, что вы должны написать еще один тест. И так далее, пока вы не поймете, что мошенничество не работает и лучше просто написать правильный путь. Если вы все еще чувствуете, что вы не закончены, вы не закончены.
источник
Я бы предположил, что ваш выбор теста не самый лучший тест.
Я бы начал с:
факториал (1) в качестве первого теста,
факториал (0) в качестве второго
факториал (-ве) как третий
а затем продолжить с нетривиальными случаями
и закончить делом переполнения.
источник
-ve
??