Стоит ли писать модульные тесты для кодов научных исследований?

89

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

Должны ли мы написать модульные тесты для исследовательских кодов?

Дэвид Кетчесон
источник
2
Это немного открытый вопрос, не так ли?
байта
2
Как и во всех «правилах», всегда применяется доза критического мышления. Спросите себя, есть ли у определенной процедуры очевидный способ пройти юнит-тестирование. Если нет, то либо модульное тестирование не имеет смысла на этом этапе, либо дизайн кода был плохим. В идеале одна подпрограмма выполняет одну задачу настолько независимую, насколько это возможно, от других подпрограмм, но иногда ее приходится менять.
Лагербаер
В аналогичном ключе есть хорошее обсуждение вопроса о stackoverflow .
naught101

Ответы:

85

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

Затем я начал использовать Test Driven Development и обнаружил, что это полное откровение. Теперь я твердо убежден, что у меня нет времени не писать юнит-тесты .

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

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

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

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

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

Для ясности, а так как @ naught101 спросил ...

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

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

Марк Бут
источник
7
Марк, о каком коде ты здесь говоришь? Многоразовая модель? Я считаю, что это обоснование на самом деле не применимо к таким вещам, как код анализа поисковых данных, где вам действительно нужно много перепрыгивать, и часто никогда не ожидать повторного использования кода где-либо еще.
naught101
35

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

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

hChrrr

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

Существует много тестов такого рода, в том числе тесты консервации и консистенции. Операция ничем не отличается от регрессии (вы запускаете тест и проверяете выходные данные на соответствие стандарту), но стандартный вывод получается из спецификации, а не из предыдущего запуска.

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

С тех пор, как я прочитал о разработке через тестирование в Code Complete, 2-е издание , я использую инфраструктуру модульного тестированиякак часть моей стратегии развития, и это значительно повысило мою производительность за счет сокращения количества времени, которое я тратил на отладку, потому что различные тесты, которые я пишу, являются диагностическими. В качестве дополнительного преимущества я гораздо увереннее в своих научных результатах и ​​неоднократно использовал свои модульные тесты для защиты своих результатов. Если в модульном тесте возникает ошибка, я могу довольно быстро выяснить, почему. Если мое приложение дает сбой и все мои модульные тесты пройдены, я делаю анализ покрытия кода, чтобы увидеть, какие части моего кода не выполняются, а также перебираю код с помощью отладчика, чтобы точно определить источник ошибки. Затем я пишу новый тест, чтобы убедиться, что ошибка остается исправленной.

Многие из тестов, которые я пишу, не являются чисто юнит-тестами. Строго определенные юнит-тесты должны выполнять функции одной функции. Когда я могу легко протестировать одну функцию, используя фиктивные данные, я делаю это. В других случаях я не могу легко смоделировать данные, необходимые для написания теста, который выполняет функциональность данной функции, поэтому я протестирую эту функцию вместе с другими в интеграционном тесте. Интеграционные тестыпроверить поведение нескольких функций одновременно. Как указывает Мэтт, научные коды часто представляют собой совокупность взаимосвязанных функций, но часто определенные функции вызываются последовательно, и могут быть написаны модульные тесты для проверки выходных данных на промежуточных этапах. Например, если мой производственный код вызывает пять последовательных функций, я напишу пять тестов. Первый тест вызовет только первую функцию (так что это модульный тест). Затем второй тест вызовет первую и вторую функции, третий тест вызовет первые три функции и так далее. Даже если бы я мог написать модульные тесты для каждой отдельной функции в моем коде, я бы в любом случае написал интеграционные тесты, потому что ошибки могут возникать при объединении различных модульных частей программы. Наконец, после написания всех модульных и интеграционных тестов, я думаю, мне нужно Я заверну свои примеры в модульных тестах и ​​использую их для регрессионного тестирования, потому что я хочу, чтобы мои результаты были повторяемыми. Если они не повторяются, и я получаю разные результаты, я хочу знать, почему. Провал регрессионного теста не может быть реальной проблемой, но это заставит меня выяснить, заслуживают ли новые результаты такой же достоверности, как старые.

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

Джефф Оксберри
источник
связанный вопрос programmers.stackexchange.com/questions/196362/…
siamii
Считаете ли вы достаточно интеграционные тесты или вам нужно написать отдельные модульные тесты?
Сиам
Я бы написал отдельные модульные тесты, где это возможно и возможно. Это облегчает отладку и обеспечивает разделенный код (что вам и нужно).
Джефф Оксберри
19

По моему опыту, поскольку сложность кодов научных исследований возрастает, в программировании требуется очень модульный подход. Это может быть болезненным для кодов с большой и древней базой ( f77кто-нибудь?), Но это необходимо для продвижения вперед. Поскольку модуль строится вокруг определенного аспекта кода (для приложений CFD, например, «Граничные условия» или «Термодинамика»), модульное тестирование очень важно для проверки новой реализации и выявления проблем и дальнейших разработок программного обеспечения.

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

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

FrenchKheldar
источник
У вас есть какие-то конкретные примеры или критерии для выбора, какие части для модульного тестирования (а какие нет)?
Дэвид Кетчон
@DavidKetcheson Мой опыт ограничен приложением и языком, который мы используем. Так что для нашего общего CFD-кода с примерно 200 тыс. Строк, в основном F90, мы пытались в течение последнего года или двух реально изолировать некоторые функциональные возможности кода. Создание модуля и использование его во всем коде не позволяет этого достичь, поэтому нужно по-настоящему сопоставить эти модули и практически сделать их библиотеками. Таким образом, только очень немногие операторы USE и все соединения с остальным кодом выполняются через обычные вызовы. Подпрограммы, которые вы можете протестировать, конечно, как и остальные библиотеки.
FrenchKheldar
@DavidKetcheson Как я уже сказал в своем ответе, граничные условия и термодинамика были двумя аспектами нашего кода, которые нам удалось по-настоящему изолировать, и поэтому тестирование модулей имело смысл. В более общем смысле я бы начал с чего-то маленького и постарался сделать это чисто. В идеале это работа для 2 человек. Один человек пишет процедуры и документацию, описывающую интерфейс, другой должен писать модульный тест, в идеале, не глядя на исходный код и следуя только описанию интерфейса. Таким образом, цель рутины проверяется, но я понимаю, что это нелегко организовать.
FrenchKheldar
1
Почему бы не включить другие виды тестирования программного обеспечения (интеграция, система) в дополнение к модульному тестированию? Помимо времени и затрат, не будет ли это наиболее полным техническим решением? Мои ссылки: 1 (раздел 3.4.2) и 2 (страница 5). Другими словами, не должен ли Исходный код тестироваться традиционными уровнями тестирования программного обеспечения 3 («Уровни тестирования»)?
ximiki
14

Модульное тестирование научных кодов полезно по ряду причин.

Три, в частности:

  • Модульные тесты помогают другим людям понять ограничения вашего кода. По сути, модульные тесты являются формой документации.

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

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

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

Отмечу, что вы упоминаете регрессионное тестирование в своем вопросе. Во многих случаях регрессионное тестирование достигается с помощью автоматического регулярного выполнения модульных тестов и / или интеграционных тестов (которые проверяют правильность работы фрагментов кода при объединении; в научных вычислениях это часто делается путем сравнения выходных данных с экспериментальными данными или результаты более ранних программ, которым доверяют). Похоже, вы уже успешно используете интеграционные или модульные тесты на уровне больших сложных компонентов.

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

Вы можете найти описание, доказательства и ссылки в Разделе 7 «План ошибок» статьи, которую я в соавторстве написал « Лучшие практики для научных вычислений», - в ней также представлена ​​дополнительная концепция защитного программирования.

Нил Чу Хонг
источник
9

В моем deal.II классов я преподаю , что программное обеспечение , которое не имеет тестов не работает правильно (и перейти к стрессу , что я намеренно сказал « ничего не работает правильно», а не " может не работать правильно).

Конечно, я живу по мантре - вот как это делается. Я выполнил 2500 тестов с каждым коммитом ;-)

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

Вольфганг Бангерт
источник
Почему вы предлагаете эту иерархию (единица для более низкого уровня, регрессия для более высокого уровня) по сравнению с традиционными уровнями в тестировании программного обеспечения?
ximiki
@ ximiki - я не хочу Я говорю, что существуют тесты для спектра, который будет включать все категории, перечисленные в вашей ссылке.
Вольфганг Бангерт
8

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

Стефано Борини
источник
7

Абсолютно!

Что, тебе мало?

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

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

Дэн
источник
+1: «Работа в таких этапах означает, что когда вы сталкиваетесь с проблемой, у вас есть только самый последний« этап »кода для тестирования, более ранние этапы уже были протестированы».
ximiki
5

Да.

Идея, что любой код написан без юнит-тестов, анафема. Если вы не докажете, что ваш код верен, а затем докажете, что код правильный = P

aterrel
источник
3
... а потом ты докажешь, что доказательство правильное, и ... теперь это глубокая кроличья нора.
JM
2
Черепахи до самого конца заставляют Дейкстру гордиться!
aterrel
2
Просто решите общий случай, и тогда ваше доказательство окажется правильным! Тор черепах!
Aesin
5

Я бы подошел к этому вопросу прагматично, а не догматично. Задайте себе вопрос: «Что может пойти не так в функции X?» Представьте себе, что происходит с выводом, когда вы вводите в код некоторые типичные ошибки: неправильный префактор, неправильный индекс ... И затем пишете модульные тесты, которые могут обнаружить такую ​​ошибку. Если для данной функции нет способа написать такие тесты без повторения кода самой функции, то не надо - но подумайте о тестах на следующем более высоком уровне.

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

khinsen
источник
Независимо от того, тестируете ли вы вручную или автоматически с помощью модульных тестов, у вас точно такие же проблемы с представлением с плавающей запятой. Я очень рекомендую Ричард Харрис отличной серию статей в ACCU «s журнал перегрузки .
Марк Бут
«Если для данной функции нет способа написать такие тесты, не повторяя код самой функции, то не используйте». Можете ли вы уточнить? Пример прояснит это для меня.
ximiki
5

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

  • Использование памяти должно быть проверено. Каждая функция, которая выделяет память, должна быть проверена, чтобы убедиться, что функции, которые хранят и извлекают данные в эту память, работают правильно. Это еще важнее в мире графических процессоров.
  • Хотя кратко упомянуто ранее, крайние случаи тестирования чрезвычайно важны. Думайте об этих тестах так же, как вы проверяете результат любого вычисления. Убедитесь, что код ведет себя по краям и изящно завершается с ошибкой (однако вы определяете это в симуляции), когда входные параметры или данные выходят за допустимые границы. Мышление, вовлеченное в написание такого рода тестов, помогает обострить вашу работу и может быть одной из причин того, что вы редко находите кого-то, кто написал модульные тесты, но не находит этот процесс полезным.
  • Используйте тестовый фреймворк (как упомянул Джефф, который предоставил хорошую ссылку). Я использовал тестовый фреймворк BOOST в сочетании с системой CMest CTest и могу рекомендовать его как простой способ быстрого написания модульных тестов (а также валидационных и регрессионных тестов).
user69
источник
+1: «Удостоверьтесь, что код ведет себя по краям и грациозно завершается ошибкой (однако вы определяете это при моделировании), когда входные параметры или данные выходят за допустимые границы».
ximiki
5

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

Первые две версии рухнули под их собственным весом и умножением взаимосвязей.

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

dmckee
источник
3

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

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

Однако мне удалось разработать все более сложный набор фрагментов, показывающих, как работает анализатор (Boost Spirit) для химических формул, в качестве модульных тестов для различных выражений.

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

То, что делает это не просто учебным упражнением и настоящим набором регрессии, - это два фактора: модульные тесты хранятся в основном источнике и выполняются как часть других тестов для этого приложения (и да, они получили побочный эффект от Boost). Дух изменился спустя 2 года) - поскольку скопированный и вставленный код был минимально изменен в реальном приложении, в нем могли быть комментарии, относящиеся к модульным тестам, чтобы помочь кому-то их синхронизировать.

Энди Дент
источник
2

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

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

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

В таких случаях автоматизированные тесты часто имеют слишком высокое соотношение затрат и выгод. Я рекомендую что-то лучше: написать тестовые программы. Например, я написал скрипт Python среднего размера для создания данных о результатах, таких как гистограммы размеров ребер и углов сетки, площади наибольшего и наименьшего треугольника и их соотношения и т. Д.

Я могу использовать его для оценки входных и выходных сеток во время нормальной работы и использовать его для проверки работоспособности после того, как я изменил алгоритм. Когда я меняю алгоритм, я не всегда знаю, лучше ли новый результат, потому что часто нет абсолютной меры, какое приближение является лучшим. Но, генерируя такие метрики, я могу рассказать о некоторых факторах, которые лучше похожи на «Новый вариант в конечном итоге имеет лучшее соотношение углов, но худшую скорость сходимости».

алло
источник