Я часто работаю с очень числовыми / математическими программами, где точный результат функции сложно предсказать заранее.
Пытаясь применить TDD к этому виду кода, я часто нахожу написание тестируемого кода значительно проще, чем написание модульных тестов для этого кода, потому что единственный способ узнать ожидаемый результат - применить сам алгоритм (будь то в моем голову, на бумаге или на компьютере). Это неправильно, потому что я эффективно использую тестируемый код для проверки своих модульных тестов, а не наоборот.
Существуют ли известные методы написания модульных тестов и применения TDD, когда результат тестируемого кода трудно предсказать?
(Реальный) пример кода с трудно предсказуемыми результатами:
Функция, weightedTasksOnTime
которая, учитывая объем работы, выполненной за день workPerDay
в диапазоне (0, 24], текущее время initialTime
> 0, и список задач taskArray
, каждая из которых имеет время для завершения свойства time
> 0, срок выполнения due
и значение важности importance
; возвращает нормализованное значение в диапазоне [0, 1], представляющее важность задач, которые могут быть выполнены до их due
даты, если каждая задача выполнена в порядке, заданном taskArray
, начиная с initialTime
.
Алгоритм реализации этой функции относительно прост: перебирать задачи в taskArray
. Для каждой задачи добавьте time
в initialTime
. Если новое время < due
, добавьте importance
в аккумулятор. Время корректируется обратным workPerDay. Прежде чем вернуть аккумулятор, разделите его на сумму значений задач для нормализации.
function weightedTasksOnTime(workPerDay, initialTime, taskArray) {
let simulatedTime = initialTime
let accumulator = 0;
for (task in taskArray) {
simulatedTime += task.time * (24 / workPerDay)
if (simulatedTime < task.due) {
accumulator += task.importance
}
}
return accumulator / totalImportance(taskArray)
}
Я полагаю, что вышеуказанную проблему можно упростить, сохранив ее ядро, удалив workPerDay
и требование нормализации, чтобы дать:
function weightedTasksOnTime(initialTime, taskArray) {
let simulatedTime = initialTime
let accumulator = 0;
for (task in taskArray) {
simulatedTime += task.time
if (simulatedTime < task.due) {
accumulator += task.importance
}
}
return accumulator
}
Этот вопрос касается ситуаций, когда тестируемый код не является повторной реализацией существующего алгоритма. Если код является повторной реализацией, он сам по себе легко предсказывает результаты, потому что существующие надежные реализации алгоритма действуют как естественный тестовый оракул.
источник
Ответы:
Есть две вещи, которые вы можете проверить в трудном для тестирования коде. Во-первых, вырожденные случаи. Что произойдет, если у вас нет элементов в массиве задач, или только один, или два, но один из них истек срок исполнения и т. Д. Все, что проще, чем ваша настоящая проблема, но все же разумно рассчитать вручную.
Второе - это проверка вменяемости. Эти проверки вы делаете , когда вы не знаете , если ответ верно , но вы определенно бы знать , если это не так . Это такие вещи, как время должно двигаться вперед, значения должны находиться в разумных пределах, проценты должны составлять до 100 и т. Д.
Да, это не так хорошо, как полный тест, но вы будете удивлены тем, как часто вы путаетесь в проверках работоспособности и вырожденных случаях, что выявляет проблему в вашем полном алгоритме.
источник
Раньше я писал тесты для научного программного обеспечения с трудно предсказуемыми результатами. Мы много использовали Метаморфические Отношения. По сути, есть вещи, которые вы знаете о том, как должно работать ваше программное обеспечение, даже если вы не знаете точных числовых результатов.
Возможный пример для вашего случая: если вы уменьшаете объем работы, которую вы можете выполнять каждый день, тогда общий объем работы, который вы можете выполнять, в лучшем случае останется прежним, но, вероятно, уменьшится. Так что запустите функцию для ряда значений
workPerDay
и убедитесь, что связь верна.источник
В других ответах есть хорошие идеи для разработки тестов для грани или случая ошибки. Для других использование самого алгоритма не идеально (очевидно), но все же полезно.
Он обнаружит, изменился ли алгоритм (или данные, от которых он зависит)
Если изменение является случайностью, вы можете откатить коммит. Если изменение было преднамеренным, вам необходимо вернуться к модульному тестированию.
источник
Так же, как вы пишете модульные тесты для любого другого вида кода:
Если ваш код не содержит какой-либо случайный элемент или не является детерминированным (т. Е. Он не будет выдавать одинаковые выходные данные при одинаковых входных данных), он может быть проверен модулем.
Избегайте побочных эффектов или функций, на которые влияют внешние силы. Чистые функции легче тестировать.
источник
Unless your code involves some random element
Хитрость заключается в том, чтобы сделать ваш генератор случайных чисел введенной зависимостью, чтобы вы могли затем заменить его на генератор чисел, который дает именно тот результат, который вы хотите получить. Это позволяет вам снова провести точный тест - считая сгенерированные числа как входные параметры.not deterministic (i.e. it won't produce the same output given the same input)
Поскольку модульное тестирование должно начинаться с контролируемой ситуации, оно может быть недетерминированным, только если в нем есть случайный элемент, который затем можно внедрить. Я не могу думать о других возможностях здесь.if(x == x)
, это бессмысленное сравнение. Вам нужно, чтобы ваши два результата ( фактические : исходили из кода; ожидаемые : исходили из ваших внешних знаний) были независимы друг от друга.Обновление из-за опубликованных комментариев
Для краткости первоначальный ответ был удален - его можно найти в истории редактирования.
Во-первых, TL; DR, чтобы избежать длинного ответа:
Основная проблема заключается в том, что вы не проводите разделение между заказчиком и разработчиком (и аналитиком - хотя эту роль может также представлять разработчик).
Вы должны различать тестирование кода и тестирование бизнес-требований.
Например, клиент хочет, чтобы он работал как [это] . Тем не менее, разработчик неправильно понимает, и он пишет код, который делает [это] .
Поэтому разработчик напишет модульные тесты, которые тестируют, если [это] работает как ожидалось. Если он разработал приложение правильно, его модульные тесты пройдут, даже если приложение не делает [этого] , чего ожидал клиент.
Если вы хотите проверить ожидания клиента (бизнес-требования), это необходимо сделать отдельным (и более поздним) шагом.
Простой рабочий процесс разработки, который покажет вам, когда следует запускать эти тесты:
Вы можете спросить, в чем смысл проведения двух отдельных тестов, когда заказчик и разработчик совпадают. Поскольку от разработчика к покупателю не происходит «передачи», тесты запускаются один за другим, но они по-прежнему являются отдельными этапами.
Если вы хотите проверить правильность самого алгоритма, это не является частью работы разработчика . Это забота клиента, и клиент проверит это с помощью приложения.
Как предприниматель и ученый, вы можете пропустить важное различие, которое подчеркивает различные обязанности.
источник
Тестирование недвижимости
Иногда математические функции лучше обслуживаются «тестированием свойств», чем традиционным модульным тестированием на основе примеров. Например, представьте, что вы пишете модульные тесты для чего-то вроде целочисленной функции «умножения». Хотя сама функция может показаться очень простой, но если это единственный способ умножения, как вы тщательно протестируете ее без логики в самой функции? Вы можете использовать гигантские таблицы с ожидаемыми входами / выходами, но это ограничено и подвержено ошибкам.
В этих случаях вы можете проверить известные свойства функции вместо того, чтобы искать конкретные ожидаемые результаты. Для умножения вы можете знать, что умножение отрицательного числа и положительного числа должно приводить к отрицательному числу, и что умножение двух отрицательных чисел должно приводить к положительному числу и т. Д. Использование рандомизированных значений и последующая проверка того, что эти свойства сохраняются для всех Проверка значений - это хороший способ проверить такие функции. Как правило, вам нужно проверить более одного свойства, но вы часто можете определить конечный набор свойств, которые вместе проверяют правильность поведения функции, не обязательно зная ожидаемый результат для каждого случая.
Одно из лучших введений в тестирование свойств, которое я видел, это на F #. Надеемся, что синтаксис не является препятствием для понимания объяснения техники.
источник
Соблазнительно написать код, а затем посмотреть, выглядит ли результат «правильно», но, как вы правильно поняли, это не очень хорошая идея.
Когда алгоритм сложен, вы можете сделать несколько вещей, чтобы упростить ручной расчет результата.
Используйте Excel. Настройте электронную таблицу, которая выполняет некоторые или все расчеты за вас. Держите это достаточно простым, чтобы вы могли видеть шаги.
Разделите ваш метод на меньшие тестируемые методы, каждый со своими собственными тестами. Если вы уверены, что меньшие детали работают, используйте их для ручной обработки следующего шага.
Используйте совокупные свойства для проверки работоспособности. Например, скажем, у вас есть калькулятор вероятности; Вы можете не знать, какими должны быть индивидуальные результаты, но вы знаете, что все они должны составлять до 100%.
Грубая сила. Напишите программу, которая генерирует все возможные результаты, и убедитесь, что ни один из них не лучше того, что генерирует ваш алгоритм.
источник
TL; DR
Перейдите в раздел «Сравнительное тестирование» за советом, которого нет в других ответах.
истоки
Начните с тестирования случаев, которые должны быть отклонены алгоритмом (например, с нулевым или отрицательным значением
workPerDay
), и случаев, которые являются тривиальными (например, пустойtasks
массив).После этого вы хотите сначала протестировать самые простые случаи. Для
tasks
ввода нам нужно проверить разные длины; должно быть достаточно проверить 0, 1 и 2 элемента (2 относится к категории «многие» для этого теста).Если вы можете найти входные данные, которые можно рассчитать мысленно, это хорошее начало. Техника, которую я иногда использую, состоит в том, чтобы начать с желаемого результата и вернуться (в спецификации) к входам, которые должны дать этот результат.
Сравнительное тестирование
Иногда отношение выхода к входу не очевидно, но у вас есть предсказуемая связь между различными выходами, когда один вход изменяется. Если я правильно понял пример, то добавление задачи (без изменения других входных данных) никогда не увеличит долю выполненной работы вовремя, поэтому мы можем создать тест, который вызывает функцию дважды - один раз с дополнительной задачей и один раз без дополнительной задачи. - и утверждает неравенство между двумя результатами.
откаты
Иногда мне приходилось прибегать к длинному комментарию, показывающему результаты, вычисленные вручную, в шагах, соответствующих спецификации (такой комментарий обычно длиннее тестового примера). Наихудший случай - когда вам нужно поддерживать совместимость с более ранней реализацией на другом языке или для другой среды. Иногда вам просто нужно пометить данные теста чем-то вроде
/* derived from v2.6 implementation on ARM system */
. Это не очень хорошо, но может быть приемлемо в качестве теста на точность при портировании или в качестве кратковременного опоры.Напоминания
Наиболее важным атрибутом теста является его читаемость - если входные и выходные данные непрозрачны для читателя, тогда тест имеет очень низкое значение, но если читателю помогают понять взаимосвязи между ними, тогда тест служит двум целям.
Не забудьте использовать подходящие «приблизительно равные» для неточных результатов (например, с плавающей точкой).
Избегайте чрезмерного тестирования - добавляйте тест только в том случае, если он охватывает что-то (например, граничное значение), чего не достигли другие тесты.
источник
Нет ничего особенного в такой сложной для тестирования функции. То же самое относится к коду, использующему внешние интерфейсы (например, REST API стороннего приложения, которое не находится под вашим контролем и, конечно, не подлежит проверке вашим набором тестов), или к использованию сторонней библиотеки, в которой вы не уверены в точный байтовый формат возвращаемых значений).
Это вполне допустимый подход - просто запустить свой алгоритм для некоторого вменяемого ввода, посмотреть, что он делает, убедиться в правильности результата и инкапсулировать ввод и результат в качестве контрольного примера. Вы можете сделать это для нескольких случаев и таким образом получить несколько образцов. Попробуйте сделать входные параметры как можно более разными. В случае внешнего вызова API вы должны сделать несколько вызовов против реальной системы, отследить их с помощью какого-либо инструмента, а затем смоделировать их в своих модульных тестах, чтобы увидеть, как ваша программа реагирует - это то же самое, что просто выбрать несколько запускает код планирования задачи, проверяя их вручную, а затем жестко кодирует результат в ваших тестах.
Затем, очевидно, приведите в крайних случаях, таких как (в вашем примере) пустой список задач; такие вещи.
Ваш набор тестов может быть не таким хорошим, как метод, в котором вы можете легко предсказать результаты; но все же на 100% лучше, чем никакой тестовый набор (или просто тест на дым).
Если ваша проблема, однако, является то, что вы найдете его трудно решить , является ли результат является правильным, то это совсем другая проблема. Например, скажем, у вас есть метод, который определяет, является ли произвольно большое число простым. Вы вряд ли можете бросить любое случайное число в него, а затем просто «посмотреть», если результат правильный (при условии, что вы не можете определить простоту в своей голове или на листе бумаги). В этом случае вы действительно мало что можете сделать - вам нужно либо получить известные результаты (например, некоторые большие простые числа), либо реализовать функциональность с помощью другого алгоритма (возможно, даже другой команды - НАСА, кажется, любит это) и надеюсь, что если любая из реализаций содержит ошибки, по крайней мере, ошибка не приводит к таким же ошибочным результатам.
Если это обычное дело для вас, вам нужно хорошо поговорить с инженерами по требованиям. Если они не могут сформулировать ваши требования так, чтобы их было легко (или вообще возможно) проверить, то когда вы узнаете, закончили ли вы?
источник
Другие ответы хороши, поэтому я попытаюсь затронуть некоторые моменты, которые они коллективно упустили до сих пор.
Я написал (и тщательно протестировал) программное обеспечение для обработки изображений с помощью Synthetic Aperture Radar (SAR). Это научный / числовой характер (много геометрии, физики и математики).
Пара советов (для общего научного / числового тестирования):
1) Используйте обратное. Что
fft
из[1,2,3,4,5]
? Без понятия. Чтоifft(fft([1,2,3,4,5]))
? Должно быть[1,2,3,4,5]
(или близко к нему, ошибки с плавающей точкой могут появляться). То же самое относится и к 2D-случаю.2) Используйте известные утверждения. Если вы пишете детерминантную функцию, может быть трудно сказать, что это за детерминант случайной матрицы 100x100. Но вы знаете, что определитель единичной матрицы равен 1, даже если он равен 100x100. Вы также знаете, что функция должна возвращать 0 в необратимой матрице (например, 100x100, заполненное всеми 0).
3) Используйте грубые утверждения вместо точных утверждений. Я написал некоторый код для указанной обработки SAR, который регистрировал бы два изображения, создавая связующие точки, которые создают сопоставление между изображениями, а затем делал деформацию между ними, чтобы они соответствовали друг другу. Это может зарегистрироваться на уровне субпикселя. Априори сложно что- либо сказать о том, как может выглядеть регистрация двух изображений. Как вы можете это проверить? Вещи как:
поскольку вы можете зарегистрироваться только на перекрывающихся частях, зарегистрированное изображение должно быть меньше или равно вашему наименьшему изображению, а также:
поскольку изображение, зарегистрированное для самого себя, должно быть ЗАКРЫТО для самого себя, но вы можете столкнуться с некоторыми ошибками с плавающей запятой из-за имеющегося алгоритма, поэтому просто проверьте, чтобы каждый пиксель находился в пределах +/- 5% диапазона, в котором могут находиться пиксели. (0-255 - оттенки серого, обычные при обработке изображений). Результат должен быть как минимум того же размера, что и ввод.
Вы можете даже просто пройти тест на курение (то есть позвонить и убедиться, что он не разбился). В целом, этот метод лучше подходит для более крупных тестов, где конечный результат не может быть (легко) рассчитан априори к запуску теста.
4) Используйте ИЛИ СОХРАНИТЕ случайное число семян для вашего RNG.
Запускается ли должны быть воспроизводимыми. Однако неверно, что единственный способ получить воспроизводимый прогон - это предоставить конкретное начальное число генератору случайных чисел. Иногда тестирование на случайность является ценным. Я видел / слышал об ошибках в научном коде , которые возникают в вырожденных случаях , которые были случайным образом сгенерированных (в сложных алгоритмах это может быть трудно понять , что вырожденный случай даже является). Вместо того, чтобы всегда вызывать вашу функцию с одним и тем же начальным числом, создайте случайное начальное число, а затем используйте это начальное значение и запишите значение начального числа. Таким образом, каждый прогон имеет различное случайное начальное число, но если вы получаете сбой, вы можете повторно запустить результат, используя начальное число, которое вы зарегистрировали для отладки. Я фактически использовал это на практике, и это уничтожило ошибку, поэтому я решил упомянуть об этом. По общему признанию это произошло только один раз, и я уверен, что это не всегда стоит делать, поэтому используйте эту технику с осторожностью. Случайно с одним и тем же семенем всегда безопасно. Недостаток (в отличие от постоянного использования одного и того же начального числа): вы должны записывать свои тестовые прогоны. Перевернутая сторона: правильность и ошибка с ядерным оружием.
Ваш конкретный случай
1) Проверьте, что пустое значение
taskArray
возвращает 0 (известное утверждение).2) Генерация случайных ввода таким образом, что
task.time > 0
,task.due > 0
, иtask.importance > 0
для всехtask
х, и утверждают , результат больше , чем0
(грубые утверждают, случайным образом входа) . Вам не нужно сходить с ума и генерировать случайные начальные числа, ваш алгоритм просто не достаточно сложен, чтобы это оправдать. Существует около 0 шансов, что это окупится: просто сделайте тест простым.3) Проверьте, если
task.importance == 0
для всехtask
s, то результат0
(известный как assert)4) Другие ответы касались этого, но это может быть важно для вашего конкретного случая: если вы создаете API для использования пользователями за пределами вашей команды, вам необходимо протестировать вырожденные случаи. Например, если
workPerDay == 0
, убедитесь, что вы выдаваете прекрасную ошибку, которая сообщает пользователю, что это неверный ввод. Если вы не создаете API, и он предназначен только для вас и вашей команды, вы, вероятно, можете пропустить этот шаг и просто отказаться называть его в случае вырожденного случая.НТН.
источник
Включите тестирование утверждений в свой набор модульных тестов для тестирования вашего алгоритма на основе свойств. В дополнение к написанию модульных тестов, которые проверяют определенные выходные данные, пишите тесты, предназначенные для сбоя, вызывая сбои утверждений в основном коде.
Многие алгоритмы полагаются на свои корректные доказательства на поддержание определенных свойств на всех этапах алгоритма. Если вы можете осмысленно проверить эти свойства, посмотрев на выходные данные функции, для проверки ваших свойств достаточно одного модульного тестирования. В противном случае тестирование на основе утверждений позволяет вам проверить, что реализация поддерживает свойство каждый раз, когда алгоритм его принимает.
Тестирование на основе утверждений выявит недостатки алгоритма, ошибки кодирования и ошибки реализации из-за таких проблем, как численная нестабильность. Во многих языках есть механизмы, которые удаляют утверждения во время компиляции или до интерпретации кода, так что при запуске в производственном режиме утверждения не влекут за собой снижения производительности. Если ваш код проходит модульные тесты, но в реальном случае не работает, вы можете снова включить утверждения как инструмент отладки.
источник
Некоторые из других ответов здесь очень хороши:
... Я бы добавил несколько других тактик:
Декомпозиция позволяет вам гарантировать, что компоненты вашего алгоритма будут выполнять то, что вы ожидаете от них. И «хорошее» разложение позволяет вам также убедиться, что они склеены правильно. Большое разложение обобщает и упрощает алгоритм до такой степени , что вы можете предсказать результаты (по упрощенному, общему алгоритму (ы)) вручную достаточно хорошо , чтобы писать тщательные испытания.
Если вы не можете разложить до такой степени, докажите алгоритм вне кода любыми средствами, достаточными для удовлетворения вас и ваших коллег, заинтересованных сторон и клиентов. А потом просто разложите достаточно, чтобы доказать, что ваша реализация соответствует дизайну.
источник
Это может показаться чем-то вроде идеалистического ответа, но это помогает идентифицировать различные виды тестирования.
Если строгие ответы важны для реализации, то примеры и ожидаемые ответы действительно должны быть представлены в требованиях, описывающих алгоритм. Эти требования должны быть проверены группой, и если вы не получите те же результаты, необходимо определить причину.
Даже если вы играете роль аналитика, а также исполнителя, вы должны создавать требования и проверять их задолго до написания модульных тестов, поэтому в этом случае вы будете знать ожидаемые результаты и сможете писать свои тесты соответствующим образом.
С другой стороны, если вы реализуете эту часть, которая либо не является частью бизнес-логики, либо поддерживает ответ бизнес-логики, тогда будет хорошо запустить тест, чтобы увидеть результаты и затем изменить тест, чтобы ожидать эти результаты. Конечные результаты уже проверены на соответствие вашим требованиям, поэтому, если они верны, тогда весь код, подающий эти конечные результаты, должен быть численно корректным, и в этот момент ваши модульные тесты предназначены скорее для выявления крайних случаев сбоя и будущих изменений рефакторинга, чем для подтверждения того, что данный Алгоритм дает правильные результаты.
источник
Я думаю, что в некоторых случаях вполне приемлемо следить за процессом:
Это разумный подход в любой ситуации, когда проверка правильности ответа вручную проще, чем вычисление ответа вручную из первых принципов.
Я знаю людей, которые пишут программное обеспечение для рендеринга печатных страниц, и у них есть тесты, которые проверяют, что на печатной странице установлены правильные пиксели. Единственный разумный способ сделать это - написать код для визуализации страницы, проверить, хорошо ли она выглядит, а затем записать результат в качестве регрессионного теста для будущих выпусков.
То, что вы прочитали в книге, что определенная методология поощряет написание тестовых примеров в первую очередь, не означает, что вы всегда должны делать это таким образом. Правила должны быть нарушены.
источник
В других ответах на ответы уже есть методы для того, как выглядит тест, когда конкретный результат не может быть определен вне тестируемой функции.
Что я делаю дополнительно, чего не заметил в других ответах, так это автоматически генерирую тесты:
Например, если функция принимает три параметра, каждый из которых имеет допустимый диапазон ввода [-1,1], проверьте все комбинации каждого параметра: {-2, -1.01, -1, -0.99, -0.5, -0.01, 0,0.01 , 0.5,0.99,1,1.01,2, немного больше случайных в (-1,1)}
Короче говоря: иногда низкое качество может быть субсидировано количеством.
источник