постоянная интеграция для научного программного обеспечения


22

Я не инженер по программному обеспечению. Я аспирант в области наук о Земле.

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

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

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

У меня есть пара вопросов, на которые я хотел бы получить несколько советов:

Прежде всего, краткое объяснение того, как работает программное обеспечение:

  • Программное обеспечение управляется одним XML-файлом, содержащим все необходимые настройки. Вы запускаете программу, просто передавая путь к XML-файлу в качестве входного аргумента, и он запускается и создает пару файлов с результатами. Один прогон может занять ~ 30 секунд.

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

Теперь давайте перейдем к моим вопросам:

  1. юнит-тесты, интеграционные тесты, сквозные тесты? Моё программное обеспечение теперь содержит около 30 000 строк кода с сотнями функций и ~ 80 классов. Мне немного странно начинать писать модульные тесты для сотен уже реализованных функций. Поэтому я подумал о том, чтобы просто создать несколько тестовых случаев. Подготовьте 10-20 различных XML-файлов и дайте программному обеспечению работать. Я думаю, это то, что называется сквозными тестами? Я часто читаю, что вы не должны этого делать, но, может быть, это нормально, если у вас уже есть работающее программное обеспечение? Или просто глупо пытаться добавить CI к уже работающему программному обеспечению.

  2. Как вы пишете модульные тесты, если параметры функции сложно создать? Предположим, у меня есть функция, double fun(vector<Class_A> a, vector<Class_B>)и обычно мне нужно сначала прочитать несколько текстовых файлов, чтобы создать объекты типа Class_Aи Class_B. Я думал о создании некоторых фиктивных функций, например, Class_A create_dummy_object()без чтения в текстовых файлах. Я также думал о реализации какой-то сериализации . (Я не планирую тестировать создание объектов класса, поскольку они зависят только от нескольких текстовых файлов)

  3. Как писать тесты, если результаты сильно различаются? Мое программное обеспечение использует большие симуляции Монте-Карло и работает итеративно. Обычно у вас есть ~ 1000 итераций, и на каждой итерации вы создаете ~ 500-20 000 экземпляров объектов на основе моделирования Монте-Карло. Если только один результат одной итерации немного отличается, все последующие итерации будут совершенно другими. Как вы справляетесь с этой ситуацией? Я полагаю, что это большой смысл против сквозных тестов, так как конечный результат сильно варьируется?

Любой другой совет с CI высоко ценится.



1
Откуда вы знаете, что ваше программное обеспечение работает правильно? Можете ли вы найти способ автоматизировать эту проверку, чтобы вы могли запускать ее при каждом изменении? Это должно быть вашим первым шагом при внедрении CI в существующий проект.
Барт ван Инген Шенау

Как вы убедились, что ваше программное обеспечение дает приемлемые результаты в первую очередь? Почему вы уверены, что это на самом деле «работает»? Ответы на оба вопроса предоставят вам много материала для тестирования вашего программного обеспечения сейчас и в будущем.
Polygnome

Ответы:


23

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

Обработка случайности: все прогоны вашего программного обеспечения ДОЛЖНЫ быть воспроизводимыми. Если вы используете технику Монте-Карло, вы должны предоставить конкретное начальное число для генератора случайных чисел.

  • Это легко забыть, например, при использовании rand()функции C, которая зависит от глобального состояния.
  • В идеале генератор случайных чисел передается как явный объект через ваши функции. randomСтандартный заголовок библиотеки C ++ 11 делает это намного проще.
  • Вместо того, чтобы делить случайное состояние между модулями программного обеспечения, я нашел полезным создать второй ГСЧ, который будет заполнен случайным числом из первого ГСЧ. Затем, если количество запросов к RNG другим модулем изменяется, последовательность, сгенерированная первым RNG, остается неизменной.

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

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

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

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

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

Ручное тестирование: особенно для сложных проблемных областей, вы не сможете протестировать все автоматически. Например, я сейчас работаю над проблемой стохастического поиска. Если я проверю, что мое программное обеспечение всегда дает один и тот же результат, я не могу улучшить его, не нарушив тесты. Вместо этого я упростила проведение ручных тестов: я запускаю программное обеспечение с фиксированным начальным числом и получаю визуализациюрезультата (в зависимости от ваших предпочтений, R, Python / Pyplot и Matlab позволяют легко получить высококачественную визуализацию ваших наборов данных). Я могу использовать эту визуализацию, чтобы убедиться, что все пошло не так, как надо. Точно так же, отслеживание прогресса вашего программного обеспечения с помощью регистрации выходных данных может быть жизнеспособным методом ручного тестирования, по крайней мере, если я смогу выбрать тип событий для регистрации.


7

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

Вы захотите (как правило) написать тесты при изменении указанных функций. Вам не нужно сидеть сложа руки и писать сотни модульных тестов для существующих функций, что было бы (в значительной степени) пустой тратой времени. Программное обеспечение (вероятно) работает нормально, как есть. Цель этих тестов состоит в том, чтобы гарантировать, что будущие изменения не нарушат старое поведение. Если вы никогда не измените определенную функцию снова, то, вероятно, никогда не стоит тратить время на ее тестирование (поскольку она в настоящее время работает, всегда работала и, вероятно, будет продолжать работать). Я рекомендую прочитать Эффективная работа с устаревшим кодомМайкл Перья на этом фронте. У него есть несколько отличных общих стратегий для тестирования уже существующих вещей, в том числе методы разрушения зависимостей, тесты характеристик (вывод функции копирования / вставки в набор тестов для обеспечения поддержки регрессионного поведения) и многое другое.

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

В идеале, нет. Вместо этого вы упрощаете создание параметров (и, следовательно, облегчаете тестирование своего проекта). По общему признанию, изменения дизайна требуют времени, и эти рефакторинги могут быть трудными для устаревших проектов, таких как ваш. TDD (Test Driven Development) может помочь в этом. Если параметры создать очень сложно, у вас будет много трудностей при написании тестов в стиле «первый тест».

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

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

Как писать тесты, если результаты сильно различаются?

Пара советов:

1) Используйте обратные (или, в более общем случае, тестирование на основе свойств). Что это такое [1,2,3,4,5]? Без понятия. Что ifft(fft([1,2,3,4,5]))? Должно быть [1,2,3,4,5](или близко к нему, ошибки с плавающей точкой могут появляться).

2) Используйте «известные» утверждения. Если вы напишите детерминантную функцию, может быть трудно сказать, что такое детерминант матрицы 100x100. Но вы знаете, что определитель единичной матрицы равен 1, даже если он равен 100x100. Вы также знаете, что функция должна возвращать 0 в необратимой матрице (например, 100x100, заполненное всеми 0).

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

EXPECT_TRUE(reg(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.

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

НТН.


2
  1. Типы испытаний

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

      Подумайте об этом наоборот: если патч, касающийся нескольких функций, нарушает один из ваших сквозных тестов, как вы собираетесь выяснить, в чем проблема?

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

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

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

    • Да, добавление CI к существующему программному обеспечению разумно и нормально.

  2. Как написать модульные тесты

    Если ваши объекты действительно дороги и / или сложны, пишите макеты. Вы можете просто связать тесты, используя mocks отдельно от тестов, использующих реальные объекты, вместо использования полиморфизма.

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

  3. Переменные результаты

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

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


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

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

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

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

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

Несколько советов:

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

1

В ответе ранее Amon уже упоминались некоторые очень важные моменты. Позвольте мне добавить еще:

1. Различия между разработкой научного программного обеспечения и коммерческого программного обеспечения

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

Программное обеспечение в большинстве случаев написано одним или несколькими лицами. Это часто пишется для конкретного проекта. Когда проект завершен и все опубликовано, во многих случаях программное обеспечение больше не требуется.

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

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

  • У тебя есть время и ресурсы?
  • Какова долгосрочная перспектива программного обеспечения? Что будет с программным обеспечением, когда вы закончите свою работу и покидаете университет?

2. Сквозные тесты

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

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

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

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

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

3. Непрерывная интеграция

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

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

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

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

4. Тестирование ваших численных алгоритмов

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

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

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


0

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

Тем не менее, то, что ценится это:

  • это лучший метод в то время, с точки зрения алгоритма?
  • насколько легко портировать на разные вычислительные платформы (разные среды HPC, версии ОС и т. д.)
  • Надежность - работает ли он на МОЕМ наборе данных?

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.