Чи варто писати одиничні тести для кодів наукових досліджень?


89

Я переконаний у цінності використання тестів, що підтверджують повну програму (наприклад, тести конвергенції), включаючи автоматизований набір тестів регресії . Прочитавши деякі книги програмування, у мене з'явилося нудне відчуття, що я "повинен" написати одиничні тести (тобто тести, які підтверджують правильність однієї функції і не мають значення для виконання всього коду для вирішення проблеми). . Однак не завжди здається, що одиничні випробування не відповідають науковим кодам, і в кінцевому підсумку відчуваються штучними або схожими на марну трату часу.

Чи слід писати одиничні тести для дослідницьких кодів?


2
Це трохи відкрите питання, чи не так?
квіте

2
Як і у всіх "правилах", завжди застосовується доза критичного мислення. Запитайте себе, чи є певна рутина очевидним способом тестування одиниці. Якщо ні, то або тест одиниці не має сенсу в цей момент, або дизайн коду був поганим. В ідеалі, одна рутина виконує одне завдання настільки незалежно від інших процедур, наскільки це можливо, але це потрібно періодично торгувати.
Lagerbaer

Існує хороша дискусія в подібній думці щодо питання про stackoverflow .
naught101

Відповіді:


85

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

Тоді я почав використовувати розробку тестових програм, і я виявив, що це повне одкровення. Зараз я твердо переконаний, що не маю часу не писати одиничні тести .

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

Кожен раз, коли я працюю зі застарілим кодом, у якого немає тестових одиниць, і доводиться щось тестувати вручну, я продовжую думати, "це було б набагато швидше, якби цей код вже мав одиничні тести". Кожен раз, коли мені доводиться намагатися додавати функціональні можливості тестування блоку до коду з високою зв'язкою, я продовжую думати, "це було б набагато простіше, якби це було написано розв'язаним способом".

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

Додаючи функціональність до старої лабораторії, часто трапляється потрапляти в лабораторію і витрачати багато годин, працюючи над наслідками необхідної їм функції, і як я можу додати цю функціональність, не впливаючи на будь-яку іншу функціональність. Код просто не налаштований, щоб дозволити тестування в режимі офлайн, тому майже все повинно бути розроблено в режимі он-лайн. Якби я намагався розвивати офлайн, то в кінцевому підсумку я мав би більше макетних об'єктів, ніж було б розумно.

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

Для ясності, а оскільки @ naught101 запитав ...

Я, як правило, працюю над програмним забезпеченням для експериментального контролю та збору даних, з деяким спеціальним аналізом даних, тому поєднання TDD з контролем редагування допомагає задокументувати як зміни базового обладнання експерименту, так і зміни вимог щодо збору даних з часом.

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


7
Марк, про який код ви тут говорите? Модель для повторного використання? Я вважаю, що це обґрунтування насправді не застосовується до таких речей, як код аналізу даних, де вам потрібно дуже багато стрибати, і часто ніколи не очікуйте повторного використання коду в іншому місці.
naught101

35

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

Я просто коротко визначу, що я маю на увазі під такими тестами. Регресійне тестування шукає зміни в існуючій поведінці (якось підтверджено), коли зміни вносяться до коду. Блок тестування запускає фрагмент коду і перевіряє, чи дає він бажаний вихід на основі специфікації. Вони не такі вже й різні, так як оригінальний тест на регресію був одиничним тестом, оскільки я мав визначити, що вихід був дійсним.

hChrrr

Ще два приклади тестування одиниць, що надходять від PyLith , - це точкове розташування, що є єдиною функцією, для якої легко виробляти синтетичні результати, і створення з’єднаних комірок нульового об'єму в сітці, що включає декілька функцій, але адресує описаний фрагмент функціональність в коді.

Існує багато тестів подібного роду, включаючи тести на консервацію та консистенцію. Операція не так відрізняється від регресії (ви запускаєте тест і перевіряєте вихід на стандартний), але стандартний вихід виходить із специфікації, а не з попереднього виконання.


4
У Вікіпедії сказано, що "Тестування одиниць, також відоме як тестування компонентів, відноситься до тестів, які перевіряють функціональність певного розділу коду, як правило, на рівні функцій". Тести конвергенції в коді кінцевих елементів очевидно не можуть бути одиничними тестами, оскільки вони включають багато функцій.
Девід Кетчесон

Тому я дав зрозуміти вгорі посту, що я сприймаю широкий погляд на одиничні тести, і "зазвичай" означає саме це.
Метт Кнеплі

Моє запитання малося на увазі в сенсі більш широко прийнятого визначення одиничних тестів. Зараз я зробив це зовсім явно у питанні.
Девід Кетчесон

Я уточнив свою відповідь
Метт Кнеплі

Останні ваші приклади стосуються того, що я задумав.
Девід Кетчесон

28

З тих пір, як я читав про розробку тестових програм у Code Complete, 2-е видання , я використовував рамки тестування одиницьяк частина моєї стратегії розвитку, і це значно збільшило мою продуктивність за рахунок скорочення часу, який я витратив на налагодження, оскільки різні тести, які я пишу, є діагностичними. Як побічна перевага, я набагато впевненіший у своїх наукових результатах і неодноразово використовував свої одиничні тести, щоб захистити свої результати. Якщо в одиничному тесті є помилка, зазвичай я можу зрозуміти, чому досить швидко. Якщо мій додаток виходить з ладу і проходять усі мої тестові одиниці, я роблю аналіз покриття коду, щоб побачити, які частини мого коду не виконуються, а також переходжу код через відладчик, щоб точно вказати джерело помилки. Потім я пишу новий тест, щоб переконатися, що помилка залишається виправленою.

Багато тестів, які я пишу, не є чистими тестовими одиницями. Суворо визначені, одиничні тести повинні виконувати функціональність однієї функції. Коли я можу легко перевірити одну функцію за допомогою макетних даних, я це роблю. В іншому випадку я не можу легко знущатися над необхідними даними, щоб написати тест, який виконує функціонування певної функції, тому я перевіряю цю функцію разом з іншими в тесті інтеграції. Інтеграційні тестиперевірити поведінку декількох функцій одночасно. Як вказує Метт, наукові коди часто є сузір'ям функцій блокування, але часто, деякі функції викликаються послідовно, і одиничні тести можуть бути написані для тестування виходу на проміжних кроках. Наприклад, якщо мій виробничий код викликає п'ять функцій послідовно, я напишу п’ять тестів. Перший тест буде викликати лише першу функцію (тобто це одиничний тест). Тоді другий тест буде викликати першу та другу функції, третій тест буде називати перші три функції тощо. Навіть якби я міг писати одиничні тести для кожної функції у своєму коді, я все одно писав би тести інтеграції, оскільки помилки можуть виникати при поєднанні різних модульних фрагментів програми. Нарешті, після написання всіх одиниць тестів та інтеграційних тестів, я думаю, мені це потрібно, Я зафіксую свої тематичні дослідження в одиничних тестах і використаю їх для тесту регресії, тому що я хочу, щоб мої результати були повторювані. Якщо вони не повторюються, і я отримую різні результати, я хочу знати, чому. Невдача регресійного тесту може не бути справжньою проблемою, але це змусить мене зрозуміти, чи є нові результати принаймні такими ж достовірними, як і старі результати.

Також поряд із тестуванням модулів є статичний аналіз коду, налагоджувачі пам’яті та компіляція з попереджувальними прапорами компілятора для пошуку простих помилок та невикористаного коду.



Чи вважаєте ви тести інтеграції достатньою чи вважаєте, що вам також потрібно написати окремі одиничні тести?
сіямі

Я б написав окремі одиничні тести там, де це можливо і можливо. Це налагоджує налагодження простіше і застосовує розв’язаний код (що саме ви хочете).
Джефф Оксберрі

19

На мій досвід, коли складність кодів наукових досліджень збільшується, виникає необхідність мати дуже модульний підхід у програмуванні. Це може бути болісним для кодів з великою та давньою основою ( f77хто?), Але потрібно рухатися вперед. Оскільки модуль будується навколо конкретного аспекту коду (для програм CFD, думаю, граничні умови або термодинаміка), тестування блоків є дуже цінним для перевірки нової реалізації та ізоляції проблем та подальших розробок програмного забезпечення.

Ці одиничні тести повинні бути на один рівень нижче перевірки коду (чи можу я відновити аналітичне рішення мого хвильового рівняння?) Та 2 рівні нижче перевірки коду (чи можу я передбачити правильні пікові значення RMS в моєму турбулентному потоці труби), просто гарантуючи, що програмування (чи правильно передані аргументи, чи вказують вказівники на потрібну річ?) та "математику" (ця підпрограма обчислює коефіцієнт тертя. Якщо я вводя набір чисел і обчислюю рішення вручну, чи дає рутина однакові результат?) правильні. По суті, на один рівень вище того, що можуть помітити компілятори, тобто основні синтаксичні помилки.

Я б точно рекомендував це хоча б для деяких важливих модулів у вашій програмі. Однак потрібно усвідомити, що це надзвичайно виснажливо і забирає багато часу, тому якщо у вас немає необмеженої людської сили, я б не рекомендував це на 100% складного коду.


Чи є у вас якісь конкретні приклади чи критерії для вибору тестів, які слід перевірити (а які ні)?
Девід Кетчесон

@DavidKetcheson Мій досвід обмежений додатком та мовою, яку ми використовуємо. Таким чином, для нашого загального цільового коду CFD, що містить близько 200 тис. Рядків, в основному, F90, ми намагалися протягом останнього року чи двох реально виділити деякі функції коду. Створення модуля та використання його у всьому коді цього не досягає, тому треба по-справжньому порівняти ці модулі і практично зробити їх бібліотеками. Отже, лише дуже мало заяв USE та всі з'єднання з рештою коду здійснюються за допомогою звичайних викликів. Рутини, які ви, звичайно, можете перевірити, як і решта бібліотеки.
ФранцузькийХельдар

@DavidKetcheson Як я вже говорив у своїй відповіді, граничні умови та термодинаміка - це два аспекти нашого коду, які нам вдалося реально виділити, і таким чином тестувати це має сенс. Більш загальним способом я б почав з чогось маленького і намагався зробити це чисто. В ідеалі це робота на 2 людини. Одна людина записує підпрограми та документацію, що описує інтерфейс, інша повинна написати тест одиниці, в ідеалі, не дивлячись на вихідний код та йдучи лише за описом інтерфейсу. Таким чином перевіряється намір рутини, але я розумію, що це непроста річ організувати.
ФранцузькийХельдар

1
Чому б не включити інші типи тестування програмного забезпечення (інтеграція, система) на додаток до тестування одиниць? Окрім часу та витрат, хіба це не буде найбільш повним технічним рішенням? Мої посилання 1 (Розділ 3.4.2) та 2 (стор. 5). Іншими словами, чи не слід перевіряти вихідний код традиційними рівнями тестування програмного забезпечення 3 ("рівні тестування")?
ximiki

14

Одиничне тестування наукових кодів корисно з різних причин.

Зокрема, три:

  • Експертні тести допомагають іншим зрозуміти обмеження вашого коду. В основному, одиничні тести є формою документації.

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

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

Більш за все, одиничні тести дають вам впевненість у тому, що результати досліджень, які ви виробляєте, використовуючи ваші коди, є достовірними та перевіреними.

Зауважу, ви згадуєте регресійне тестування у своєму питанні. У багатьох випадках тестування регресії досягається за допомогою автоматизованого, регулярного виконання одиничних тестів та / або інтеграційних тестів (які перевіряють, що фрагменти коду працюють правильно при їх поєднанні; в наукових обчисленнях це часто робиться шляхом порівняння результатів з експериментальними даними або результати попередніх програм, яким довіряють). Здається, ви вже успішно використовуєте тести інтеграції або тестування на рівні великих складних компонентів.

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

Опис, докази та посилання ви можете знайти у Розділі 7 "План помилок" статті, яку я співавтором написав " Кращі практики для наукових обчислень", - вона також вводить доповнюючу концепцію оборонного програмування.


9

У моїй угоді .II класи я вчу, що програмне забезпечення, яке не має тестів, працює не правильно (і продовжую наголошувати, що я цілеспрямовано сказав, що " працює не правильно", "не може працювати неправильно".

Звичайно, я живу за мантрою - це так, як deal.II прийшов, щоб провести 2500 тестів з кожним зобов'язанням ;-)

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


Чому ви пропонуєте цю ієрархію (одиниця для нижчих, регресія для вищої) порівняно з традиційними рівнями тестування програмного забезпечення?
ximiki

@ximiki - я не хочу цього робити. Я кажу, що тести існують у спектрі, який би включав усі категорії, перелічені у вашому посиланні.
Вольфганг Бангерт

8

Так і ні. Безумовно, єдиний тест для основних процедур базового набору інструментів, який ви використовуєте для полегшення свого життя, таких як підпрограми перетворення, відображення рядків, основна фізика та математика тощо. Що стосується класів чи функцій обчислень, вони, як правило, вимагають тривалих періодів виконання, і ви, можливо, вважаєте за краще тестувати їх як функціональні тести, а не як одиниці. Крім того, багато тестують і наголошують на тих класах та об'єктах, рівень і використання яких значно зміниться (наприклад, для оптимізації) або внутрішні деталі яких будуть змінені з будь-якої причини. Найбільш типовий приклад - клас, який обгортає величезну матрицю, зіставлену з диска.


7

Абсолютно!

Що, цього вам мало?

У науковому програмуванні більше, ніж будь-який інший вид, ми розвиваємось на основі спроб узгодити фізичну систему. Як ви дізнаєтесь, чи зробили ви це, крім тестування? Перш ніж навіть розпочати кодування, вирішіть, як ви збираєтеся використовувати свій код, і опрацюйте кілька прикладів виконання. Спробуйте зловити будь-які можливі крайові випадки. Робіть це модульно - наприклад, для нейронної мережі ви можете зробити набір тестів для одного нейрона і набір тестів для повної нейронної мережі. Таким чином, коли ви почнете писати код, ви можете переконатися, що ваш нейрон працює, перш ніж почати працювати в мережі. Робота на таких етапах означає, що коли ви стикаєтеся з проблемою, у вас є лише остання «стадія» коду для тестування, більш ранні етапи вже перевірені.

Крім того, щойно у вас є тести, якщо вам потрібно переписати код на іншій мові (наприклад, перехід на CUDA) або навіть якщо ви лише оновлюєте її, у вас вже є тестові вітрини, і ви можете використовувати їх для виготовлення переконайтеся, що обидві версії вашої програми працюють однаково.


+1: "Робота на таких етапах означає, що коли ви стикаєтеся з проблемою, у вас є лише остання" стадія "коду для тестування, попередні етапи вже перевірені."
ximiki

5

Так.

Ідея, що будь-який код пишеться без одиничних тестів, є анафемою. Якщо ви не докажете свій код правильним, а потім не доведете правильність доказу = P.


3
... а потім ти доводиш, що доказ того, що доказ правильний, і ... ось це глибока кроляча нора.
JM

2
Черепахи всю дорогу вниз роблять Dijkstra гордим!
aterrel

2
Просто вирішіть загальну справу, а потім доведіть, що ваш доказ виявиться правильним! Торус черепах!
Езін

5

Я б підійшов до цього питання прагматично, а не догматично. Задайте собі питання: "Що може піти не так у функції X?" Уявіть, що трапляється з результатом, коли ви вводите в код кілька типових помилок: неправильний збірник, неправильний індекс, а потім записуйте одиничні тести, які, ймовірно, виявлять таку помилку. Якщо для даної функції немає можливості написати такі тести, не повторюючи код самої функції, тоді не робіть, але подумайте про тести на наступному вищому рівні.

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


Незалежно від того, чи ви тестуєте вручну чи автоматично, використовуючи одиничні тести, у вас є абсолютно однакові проблеми із поданням плаваючої точки. Я дуже рекомендую Річард Харріс відмінною серію статей в ACCU «s журнал перевантаження .
Марк Бут

"Якщо для даної функції немає можливості написати такі тести, не повторюючи код самої функції, тоді не робіть". Чи можете ви докладно? Приклад міг би це зрозуміти для мене.
ximiki

5

Мені шкода Тангурена - тут мантра є «Неперевірений код - це зламаний код», і це прийшло від начальника. Замість того, щоб повторювати всі вагомі причини для тестування одиниць, я хочу лише додати декілька специфік.

  • Використання пам'яті слід перевірити. Кожна функція, яка виділяє пам'ять, повинна бути перевірена, переконавшись, що функції, які зберігають і витягують дані в цю пам'ять, роблять правильно. Це ще важливіше у світі GPU.
  • Хоча це було коротко зазначено раніше, надзвичайно важливим є тестування кращих справ. Думайте про ці тести так само, як ви перевіряєте результат будь-якого обчислення. Переконайтеся, що код поводиться на краях і виходить з ладу (однак ви це визначаєте під час моделювання), коли вхідні параметри або дані виходять за рамки прийнятних меж. Мислення, пов'язане з написанням такого роду тесту, допомагає посилити вашу роботу і може бути однією з причин того, що ви рідко знаходите когось, хто написав одиничні тести, але не вважає цей процес корисним.
  • Скористайтеся тестовою рамкою (як згадував Джефф, який надав хороший посилання). Я використовував BOOST тестову основу спільно із системою CTest CMake і можу рекомендувати її як простий спосіб швидкого написання одиничних тестів (а також перевірок валідації та регресії).

+1: "Переконайтеся, що код поводиться на краях і виходить з ладу (однак ви це визначаєте у своєму моделюванні), коли вхідні параметри або дані виходять за рамки прийнятних меж."
ximiki

5

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

Перші дві версії розпалися під власною вагою та примноженням взаємозв'язків.

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


3

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

Побудова тестового джгута для оригінального коду, вбудованого у великий модельєр з хімічної системи, була неможлива у часові рамки.

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

Останній, найскладніший тест одиниці був дуже близьким до коду, необхідного в системі, без того, щоб змінювати цей код, щоб він змістився. Таким чином, мені вдалося скопіювати код, перевірений на моєму пристрої.

Це означає, що це більше, ніж просто навчання та справжній набір регресії - два фактори - одиничні тести, що зберігаються в основному джерелі та виконуються як частина інших тестів для цього додатка (і так, вони виявили побічний ефект від Boost Дух змінюється через 2 роки) - оскільки скопійований і вставлений код був мінімально модифікований у реальній програмі, він міг містити коментарі, що відносяться до тестів одиниці, щоб допомогти комусь утримувати їх у синхронізації.


2

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

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

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

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

Я можу використовувати його для оцінки вхідних і вихідних сіток під час нормальної роботи і використовувати його для перевірки чистоти після зміни алгоритму. Коли я змінюю алгоритм, я не завжди знаю, чи кращий новий результат, тому що часто немає абсолютної міри, наближення якої найкраще. Але генеруючи такі показники, я можу розповісти про деякі фактори, що краще на кшталт "Новий варіант зрештою має кращий кутовий коефіцієнт, але гірший коефіцієнт конвергенції".

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.