Подтвердить что ты не робот

Как правильно использовать TDD для реализации численного метода?

Я пытаюсь использовать Test Driven Development для реализации моей библиотеки обработки сигналов. Но у меня есть небольшое сомнение: предположим, что я пытаюсь реализовать синус-метод (я не уверен):

  • Запишите тест (псевдокод)

    assertEqual(0, sine(0))
    
  • Напишите первую реализацию

    function sine(radians)
        return 0
    
  • Второй тест

    assertEqual(1, sine(pi))
    

В этот момент, если я:

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

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

4b9b3361

Ответ 1

В этот момент, если я:

  • реализовать реальный код, который будет работать за пределами двух простых тестов?

  • реализовать более тупой код, который будет работать только для двух простых тестов:

Ни. Я не знаю, откуда вы взяли метод "писать только один тест за раз", но он уверен, что это медленный путь.

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

Итак, напишите достаточно тестов для фактической проверки синусоиды. Два теста явно неадекватны.

В случае непрерывной функции вы должны в конечном итоге предоставить таблицу известных хороших значений. Зачем ждать?

Однако при тестировании непрерывных функций возникают некоторые проблемы. Вы не можете следовать за немой процедурой TDD.

Вы не можете проверить все значения с плавающей запятой между 0 и 2 * pi. Вы не можете проверить несколько случайных значений.

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

Однако для непрерывных функций вы должны проверить краевые и угловые случаи реализации.

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

Ответ 2

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

Как правило, у вас может быть реальный алгоритм после одного или двух случаев. Важная вещь в TDD заключается в том, что он управляет дизайном, а не алгоритмом. Когда у вас достаточно дел для удовлетворения потребностей проекта, значение в TDD значительно падает. Затем тесты больше преобразуются в закрывающие угловые случаи, чтобы ваш алгоритм был правильным во всех аспектах, о которых вы можете думать. Итак, если вы уверены в том, как построить алгоритм, подойдите к нему. Виды детских шагов, о которых вы говорите, подходят только тогда, когда вы не уверены. Принимая такие детские шаги, вы начинаете строить границы того, что должен покрывать ваш код, даже если ваша реализация на самом деле еще не реальна. Но, как я уже сказал, это больше, когда вы не знаете, как построить алгоритм.

Ответ 3

Запишите тесты, удостоверяющие Identities.

Для примера sin (x) подумайте о формуле с двумя углами и формуле полуугольника.

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

Затем подумайте о входах.

  • Разделить процесс реализации на отдельные этапы. На каждом этапе должна быть цель. Тесты для каждого этапа состоят в том, чтобы проверить эту цель. (Примечание 1)
    • Цель первого этапа - быть "грубо правильным". Для примера sin (x) это будет выглядеть как наивная реализация, использующая двоичный поиск и некоторые математические идентичности.
    • Цель второго этапа - быть "достаточно точным". Вы попробуете разные способы вычисления одной и той же функции и посмотрите, какой результат лучше.
    • Цель третьего этапа - быть "эффективной".

(Примечание 1) Заставьте его работать, сделать его правильным, быстро сделать, сделать его дешевым. - приписывается Алану Кей

Ответ 4

Я считаю, что шаг, когда вы переходите к первому варианту, - это когда вы видите, что слишком много "ifs" в вашем коде "просто для прохождения тестов". Это еще не так, только с 0 и pi.

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

Ответ 5

Вы должны составить код всех ваших модульных тестов одним ударом (на мой взгляд). Хотя идея только создания тестов, конкретно касающихся тестируемого, верна, ваша конкретная спецификация требует функции sine(), а не функции sine(), которая работает для 0 и PI.

Найдите источник, которому вы доверяете достаточно (друг математика, таблицы в конце математической книги или другая программа, которая уже реализовала функцию синуса).

Я выбрал bash/bc, потому что я слишком ленив, чтобы набрать все это вручную:-). Если бы это была функция sine(), я бы просто запустил следующую программу и вставил ее в тестовый код. Я также разместил бы копию этого script там как комментарий, так что я могу повторно использовать его, если что-то изменится (например, желаемое разрешение, если в этом случае более 20 градусов или значение PI вы хотите использовать).

#!/bin/bash
d=0
while [[ ${d} -le 400 ]] ; do
    r=$(echo "3.141592653589 * ${d} / 180" | bc -l)
    s=$(echo "s(${r})" | bc -l)
    echo "assertNear(${s},sine(${r})); // ${d} deg."
    d=$(expr ${d} + 20)
done

Выводится:

assertNear(0,sine(0)); // 0 deg.
assertNear(.34202014332558591077,sine(.34906585039877777777)); // 20 deg.
assertNear(.64278760968640429167,sine(.69813170079755555555)); // 40 deg.
assertNear(.86602540378430644035,sine(1.04719755119633333333)); // 60 deg.
assertNear(.98480775301214683962,sine(1.39626340159511111111)); // 80 deg.
assertNear(.98480775301228458404,sine(1.74532925199388888888)); // 100 deg.
assertNear(.86602540378470305958,sine(2.09439510239266666666)); // 120 deg.
assertNear(.64278760968701194759,sine(2.44346095279144444444)); // 140 deg.
assertNear(.34202014332633131111,sine(2.79252680319022222222)); // 160 deg.
assertNear(.00000000000079323846,sine(3.14159265358900000000)); // 180 deg.
assertNear(-.34202014332484051044,sine(3.49065850398777777777)); // 200 deg.
assertNear(-.64278760968579663575,sine(3.83972435438655555555)); // 220 deg.
assertNear(-.86602540378390982112,sine(4.18879020478533333333)); // 240 deg.
assertNear(-.98480775301200909521,sine(4.53785605518411111111)); // 260 deg.
assertNear(-.98480775301242232845,sine(4.88692190558288888888)); // 280 deg.
assertNear(-.86602540378509967881,sine(5.23598775598166666666)); // 300 deg.
assertNear(-.64278760968761960351,sine(5.58505360638044444444)); // 320 deg.
assertNear(-.34202014332707671144,sine(5.93411945677922222222)); // 340 deg.
assertNear(-.00000000000158647692,sine(6.28318530717800000000)); // 360 deg.
assertNear(.34202014332409511011,sine(6.63225115757677777777)); // 380 deg.
assertNear(.64278760968518897983,sine(6.98131700797555555555)); // 400 deg.

Очевидно, вам нужно будет сопоставить этот ответ с тем, что означает ваша настоящая функция. Я хочу сказать, что тест должен полностью проверить поведение кода в этой итерации. Если эта итерация должна была создать функцию sine(), которая работает только для 0 и PI, то это прекрасно. Но это будет серьезной тратой итерации, на мой взгляд.

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

Ответ 6

Строго следуя TDD, вы можете сначала реализовать самый тупой код, который будет работать. Чтобы перейти к первому варианту (для реализации реального кода), добавьте больше тестов:

assertEqual(tan(x), sin(x)/cos(x))

Если вы реализуете больше, чем это абсолютно необходимо для ваших тестов, ваши тесты не будут полностью охватывать вашу реализацию. Например, если вы выполнили всю функцию sin() только с двумя вышеуказанными тестами, вы можете случайно "разбить" ее, вернув треугольную функцию (которая почти выглядит как синусоидальная функция), и ваши тесты не смогут обнаружить ошибка.

Другая вещь, о которой вам придется беспокоиться о числовых функциях, - понятие "равенство" и необходимость иметь дело с присущей потерей точности вычислений с плавающей запятой. Это то, о чем я думал, что ваш вопрос будет состояться после прочтения только названия.:)

Ответ 7

Обратите внимание, что (в NUnit) вы также можете сделать

Assert.That(2.1 + 1.2, Is.EqualTo(3.3).Within(0.0005);

когда вы имеете дело с равенством с плавающей запятой.

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

Ответ 8

Я не знаю, на каком языке вы используете, но когда я имею дело с числовым методом, я обычно пишу простой тест, например ваш, прежде всего, чтобы убедиться, что контур правильный, а затем я подаю больше значений для покрытия случаев где я подозреваю, что все может пойти не так. В .NET NUnit 2.5 имеет приятную функцию для этого, называемую [TestCase], где вы можете подавать несколько входных значений в один и тот же тест следующим образом:

[TestCase(1,2,Result=3)]   
[TestCase(1,1,Result=2)]     
public int CheckAddition(int a, int b)   
{  
 return a+b;   
}

Ответ 9

Короткий ответ.

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

Еще один вопрос, который у вас есть, - сколько тестов вы должны писать. Вам нужно протестировать, пока страх (функция не может работать) превращается в скуку. Итак, как только вы протестировали все интересные комбинации ввода-вывода, вы сделали.