Как вы пишете модульные тесты для кода с трудно предсказуемыми результатами?

124

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

Пытаясь применить 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
}

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

PaintingInAir
источник
4
Можете ли вы привести простой пример функции, результат которой трудно предсказать?
Роберт Харви
62
FWIW вы не тестируете алгоритм. Предположительно это правильно. Вы тестируете реализацию. Тренировка вручную часто прекрасна как параллельная конструкция.
Кристиан Х
7
Существуют ситуации, когда алгоритм не может быть разумно протестирован модулем, например, если время его выполнения составляет несколько дней / месяцев. Это может случиться при решении задач NP. В этих случаях может быть более целесообразным предоставить формальное доказательство правильности кода.
Халк
12
Что-то, что я видел в очень хитром числовом коде, - это рассматривать модульные тесты только как регрессионные. Напишите функцию, запустите ее для нескольких интересных значений, проверьте результаты вручную, затем напишите модульный тест, чтобы отследить регрессии от ожидаемого результата. Код ужаса? Любопытно, что думают другие.
Chuu

Ответы:

251

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

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

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

Карл Билефельдт
источник
54
Думаю, это очень хороший совет. Начните с написания такого рода модульных тестов. По мере разработки программного обеспечения, если вы обнаружите ошибки или неправильные ответы - добавьте их в качестве модульных тестов. Сделайте то же самое, в некоторой степени, когда вы найдете однозначно правильные ответы. Постройте их с течением времени, и у вас (в конце концов) будет очень полный набор юнит-тестов, несмотря на то, что вы не
знаете,
21
Еще одна вещь, которая может быть полезна в некоторых случаях (хотя, возможно, и не в этом), это написать обратную функцию и проверить, что при цепочке ваши входные и выходные данные совпадают.
Cyberspark
7
проверка работоспособности часто дает хорошие цели для тестов на основе свойств с помощью чего-то вроде QuickCheck
jk.
10
еще одна категория тестов, которые я бы порекомендовал, это несколько, чтобы проверить непреднамеренные изменения в выводе. Вы можете «обмануть» их, используя сам код для получения ожидаемого результата, поскольку цель этого состоит в том, чтобы помочь сопровождающим, отметив, что что-то, предназначенное в качестве нейтрального изменения вывода, непреднамеренно повлияло на алгоритмическое поведение.
Дэн Нили,
5
@iFlo Не уверен, что вы пошутили, но обратное обратное уже существует. Стоит понимать, что провал теста может быть проблемой в обратной функции
lucidbrot
80

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

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

Джеймс Элдерфилд
источник
32
Метаморфические отношения - конкретный пример тестирования на основе свойств , который в целом является полезным инструментом для подобных ситуаций
Dannnno
38

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

Он обнаружит, изменился ли алгоритм (или данные, от которых он зависит)

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

user949300
источник
6
И, к сведению, такого рода тесты часто называют «регрессионными тестами» в соответствии с их назначением, и, в основном, они являются сетью безопасности для любой модификации / рефакторинга.
Pac0
21

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

  1. Найдите несколько репрезентативных тестовых случаев и протестируйте их.
  2. Найдите крайние случаи и протестируйте их.
  3. Найдите условия ошибки и проверьте их.

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

Избегайте побочных эффектов или функций, на которые влияют внешние силы. Чистые функции легче тестировать.

Роберт Харви
источник
2
Для недетерминированных алгоритмов вы можете сохранить начальное значение ГСЧ или смоделировать его, используя либо фиксированную последовательность, либо детерминированный ряд с малым расхождением, например, последовательность Халтона
wondra
14
@PaintingInAir Если невозможно проверить вывод алгоритма, может ли алгоритм быть неверным?
WolfgangGroiss
5
Unless your code involves some random elementХитрость заключается в том, чтобы сделать ваш генератор случайных чисел введенной зависимостью, чтобы вы могли затем заменить его на генератор чисел, который дает именно тот результат, который вы хотите получить. Это позволяет вам снова провести точный тест - считая сгенерированные числа как входные параметры. not deterministic (i.e. it won't produce the same output given the same input)Поскольку модульное тестирование должно начинаться с контролируемой ситуации, оно может быть недетерминированным, только если в нем есть случайный элемент, который затем можно внедрить. Я не могу думать о других возможностях здесь.
Флатер
3
@PaintingInAir: Либо или. Мой комментарий относится как к быстрому выполнению, так и к быстрому написанию тестов. Если вам понадобится три дня, чтобы вычислить один пример вручную (предположим, вы используете самый быстрый из доступных методов, не использующий код), - тогда потребуется три дня. Если вы вместо этого основали свой ожидаемый результат теста на самом фактическом коде, то тест сам по себе идет на компромисс. Это все равно что делать if(x == x), это бессмысленное сравнение. Вам нужно, чтобы ваши два результата ( фактические : исходили из кода; ожидаемые : исходили из ваших внешних знаний) были независимы друг от друга.
Флатер
2
Он все еще может быть проверен на единицу, даже если он не является детерминированным, при условии, что он соответствует спецификациям и что соответствие может быть измерено (например, распределение и разброс случайным образом). Для устранения риска возникновения аномалии может потребоваться большое количество образцов.
Маккензм
17

Обновление из-за опубликованных комментариев

Для краткости первоначальный ответ был удален - его можно найти в истории редактирования.

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

Во-первых, TL; DR, чтобы избежать длинного ответа:

Думайте об этом так:
клиент входит в McDonald's и просит гамбургер с салатом, помидорами и мылом для рук в качестве начинки. Этот приказ отдается повару, который готовит гамбургер в точности так, как просил. Клиент получает этот гамбургер, ест его, а затем жалуется повару, что это не вкусный гамбургер!

Это не вина повара - он делает только то, что прямо спросил клиент. Это не работа повара, чтобы проверить, является ли запрошенный заказ действительно вкусным . Повар просто создает то, что заказывает клиент. Заказчик обязан заказать что-нибудь вкусное .

Аналогично, задача разработчика не в том, чтобы ставить под сомнение правильность алгоритма. Их единственная задача - реализовать алгоритм по запросу.
Модульное тестирование является инструментом разработчика. Это подтверждает, что гамбургер соответствует порядку (прежде чем он покинет кухню). Он не пытается (и не должен) подтверждать, что заказанный гамбургер действительно вкусный.

Даже если вы и клиент, и повар, все равно есть значимое различие между:

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

Основная проблема заключается в том, что вы не проводите разделение между заказчиком и разработчиком (и аналитиком - хотя эту роль может также представлять разработчик).

Вы должны различать тестирование кода и тестирование бизнес-требований.

Например, клиент хочет, чтобы он работал как [это] . Тем не менее, разработчик неправильно понимает, и он пишет код, который делает [это] .

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

Если вы хотите проверить ожидания клиента (бизнес-требования), это необходимо сделать отдельным (и более поздним) шагом.

Простой рабочий процесс разработки, который покажет вам, когда следует запускать эти тесты:

  • Заказчик объясняет проблему, которую хочет решить.
  • Аналитик (или разработчик) записывает это в анализе.
  • Разработчик пишет код, который делает то, что описывает анализ.
  • Разработчик проверяет свой код (модульные тесты), чтобы увидеть, правильно ли он выполнил анализ
  • Если модульные тесты не пройдены, разработчик возвращается к разработке. Это повторяется до бесконечности, пока все тесты не пройдут.
  • Теперь, имея проверенную (подтвержденную и пройденную) кодовую базу, разработчик создает приложение.
  • Приложение предоставляется заказчику.
  • Теперь заказчик проверяет, действительно ли данное ему приложение решает проблему, которую он стремился решить (тесты QA) .

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

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

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

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

  • Если приложение не придерживается того, о чем изначально просил клиент, то последующие изменения в коде обычно делаются бесплатно ; так как это ошибка разработчика. Разработчик допустил ошибку и должен заплатить за ее исправление.
  • Если приложение выполняет то, что изначально просил клиент, но теперь клиент передумал (например, вы решили использовать другой и лучший алгоритм), изменения в кодовой базе оплачиваются клиенту , поскольку это не вина разработчика в том, что заказчик попросил что-то отличное от того, что он сейчас хочет. Ответственность (стоимость) клиента заключается в том, чтобы изменить свое мнение, и поэтому разработчики должны тратить больше усилий на разработку чего-то, о чем ранее не было согласовано.
Flater
источник
Я был бы рад видеть более подробную информацию о ситуации «если вы сами придумали алгоритм», так как я думаю, что именно эта ситуация скорее всего создаст проблемы. Особенно в ситуациях, когда не приводятся примеры «если A, то B, иначе C». (ps я не downvoter)
PaintingInAir
@PaintingInAir: Но я не могу подробно остановиться на этом, поскольку это зависит от вашей ситуации. Если вы решили создать этот алгоритм, вы, очевидно, сделали это для предоставления определенной функции. Кто просил тебя сделать это? Как они описали свой запрос? Они рассказали вам, что им нужно было сделать в определенных сценариях? (эту информацию я называю «анализом» в моем ответе). Любое полученное вами объяснение (которое привело вас к созданию алгоритма) может быть использовано для проверки того, работает ли алгоритм в соответствии с запросом. Короче говоря, можно использовать все, кроме алгоритма кода / собственного создания .
Flater
2
@PaintingInAir: опасно тесно связывать клиента, аналитика и разработчика; поскольку вы склонны пропускать важные шаги, такие как определение проблемы с самого начала . Я верю, что ты здесь делаешь. Похоже, вы хотите проверить правильность алгоритма, а не проверить, правильно ли он был реализован. Но это не так, как вы это делаете. Тестирование реализации может быть выполнено с использованием модульных тестов. Тестирование самого алгоритма заключается в использовании вашего (проверенного) приложения и проверке фактов его результатов - этот фактический тест выходит за рамки вашей кодовой базы (как и должно быть ).
Флэттер
4
Этот ответ уже огромен. Настоятельно рекомендуем попытаться найти способ переформулировать исходный контент, чтобы вы могли просто интегрировать его в новый ответ, если не хотите его выбрасывать.
jpmc26
7
Кроме того, я не согласен с вашей предпосылкой. Тесты могут и абсолютно должны выявить, когда код генерирует неправильный вывод в соответствии со спецификацией. Для тестов допустимо проверять выходные данные для некоторых известных тестовых случаев. Кроме того, повар должен знать лучше, чем принимать «мыло для рук» в качестве действующего ингредиента бургера, и работодатель почти наверняка ознакомил повара с тем, какие ингредиенты доступны.
jpmc26
9

Тестирование недвижимости

Иногда математические функции лучше обслуживаются «тестированием свойств», чем традиционным модульным тестированием на основе примеров. Например, представьте, что вы пишете модульные тесты для чего-то вроде целочисленной функции «умножения». Хотя сама функция может показаться очень простой, но если это единственный способ умножения, как вы тщательно протестируете ее без логики в самой функции? Вы можете использовать гигантские таблицы с ожидаемыми входами / выходами, но это ограничено и подвержено ошибкам.

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

Одно из лучших введений в тестирование свойств, которое я видел, это на F #. Надеемся, что синтаксис не является препятствием для понимания объяснения техники.

Аарон М. Эшбах
источник
1
Я бы предложил добавить что-то более конкретное в ваш пример повторного умножения, например, генерировать случайные квартеты (a, b, c) и подтвердить, что (ab) (cd) дает (ac-ad) - (bc-bd). Операция умножения может быть довольно нарушена и все еще поддерживать правило (отрицательное время дает положительный результат), но правило распределения предсказывает конкретные результаты.
суперкат
4

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

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

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

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

  3. Используйте совокупные свойства для проверки работоспособности. Например, скажем, у вас есть калькулятор вероятности; Вы можете не знать, какими должны быть индивидуальные результаты, но вы знаете, что все они должны составлять до 100%.

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

Ewan
источник
Для 3., допустим некоторые ошибки округления здесь. Возможно, ваша общая сумма равна 100,000001% или аналогичным близким, но не точным показателям.
Флатер
2
Я не совсем уверен насчет 4. Если вы в состоянии сгенерировать оптимальный результат для всех возможных входных комбинаций (которые вы затем используете для проверки подтверждения), то вы по сути уже способны рассчитать оптимальный результат и, следовательно, не Мне не нужен этот второй кусок кода, который вы пытаетесь проверить. В этот момент вам лучше использовать существующий генератор оптимальных результатов, поскольку он уже доказал свою эффективность. (и если он еще не доказал свою работоспособность, вы не можете полагаться на его результаты для проверки фактов ваших тестов для начала).
Флатер
6
@flater, как правило, у вас есть другие требования, а также правильность, что грубая сила не соответствует. например, производительность.
Эван
1
@ flater Я бы не хотел использовать ваш вид, кратчайший путь, шахматный движок и т. д., если вы в это верите. Но ID абсолютно азартная игра в вашей ошибке округления позволила казино весь день
Ewan
3
@ flater ты уйдешь в отставку, когда попадешь в игру с пешкой короля? просто потому, что вся игра не может быть грубой, не означает индивидуальную позицию. Тот факт, что вы используете грубую силу для правильного кратчайшего пути к одной сети, не означает, что вы знаете кратчайший путь во всех сетях
Ewan
2

TL; DR

Перейдите в раздел «Сравнительное тестирование» за советом, которого нет в других ответах.


истоки

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

После этого вы хотите сначала протестировать самые простые случаи. Для tasksввода нам нужно проверить разные длины; должно быть достаточно проверить 0, 1 и 2 элемента (2 относится к категории «многие» для этого теста).

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

Сравнительное тестирование

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

откаты

Иногда мне приходилось прибегать к длинному комментарию, показывающему результаты, вычисленные вручную, в шагах, соответствующих спецификации (такой комментарий обычно длиннее тестового примера). Наихудший случай - когда вам нужно поддерживать совместимость с более ранней реализацией на другом языке или для другой среды. Иногда вам просто нужно пометить данные теста чем-то вроде /* derived from v2.6 implementation on ARM system */. Это не очень хорошо, но может быть приемлемо в качестве теста на точность при портировании или в качестве кратковременного опоры.

Напоминания

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

Не забудьте использовать подходящие «приблизительно равные» для неточных результатов (например, с плавающей точкой).

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

Тоби Спейт
источник
2

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

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

Затем, очевидно, приведите в крайних случаях, таких как (в вашем примере) пустой список задач; такие вещи.

Ваш набор тестов может быть не таким хорошим, как метод, в котором вы можете легко предсказать результаты; но все же на 100% лучше, чем никакой тестовый набор (или просто тест на дым).

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

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

Anoe
источник
2

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

Я написал (и тщательно протестировал) программное обеспечение для обработки изображений с помощью 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, который регистрировал бы два изображения, создавая связующие точки, которые создают сопоставление между изображениями, а затем делал деформацию между ними, чтобы они соответствовали друг другу. Это может зарегистрироваться на уровне субпикселя. Априори сложно что- либо сказать о том, как может выглядеть регистрация двух изображений. Как вы можете это проверить? Вещи как:

EXPECT_TRUE(register(img1, img2).size() < min(img1.size(), img2.size()))

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

scale = 255
EXPECT_PIXEL_EQ_WITH_TOLERANCE(reg(img, img), img, .05*scale)

поскольку изображение, зарегистрированное для самого себя, должно быть ЗАКРЫТО для самого себя, но вы можете столкнуться с некоторыми ошибками с плавающей запятой из-за имеющегося алгоритма, поэтому просто проверьте, чтобы каждый пиксель находился в пределах +/- 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

Некоторые из других ответов здесь очень хороши:

  • Испытайте основание, край и угловые случаи
  • Выполните проверки вменяемости
  • Провести сравнительные испытания

... Я бы добавил несколько других тактик:

  • Разложи проблему.
  • Докажите алгоритм вне кода.
  • Проверьте, что [внешне доказанный] алгоритм реализован как спроектированный.

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

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

svidgen
источник
0

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

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

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

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

Билл К
источник
0

Я думаю, что в некоторых случаях вполне приемлемо следить за процессом:

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

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

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

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

Майкл Кей
источник
0

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

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

  1. «Случайные» входы
  2. Итерация по диапазонам данных
  3. Построение тестовых случаев из наборов границ
  4. Все выше.

Например, если функция принимает три параметра, каждый из которых имеет допустимый диапазон ввода [-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)}

Короче говоря: иногда низкое качество может быть субсидировано количеством.

Кит
источник