Як уникнути тендітних одиничних тестів?


24

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

Що я шукаю - це остаточний спосіб написання керованих і ремонтованих тестів.

Каркаси


Це набагато краще підходить програмістам.StackExchange, IMO ...
IАнотація

Відповіді:


21

Не думайте про них як про "тестові ламані одиниці", тому що вони не є.

Це специфікації, які ваша програма більше не підтримує.

Не сприймайте це як "виправлення тестів", а як "визначення нових вимог".

Тести повинні спочатку визначати вашу програму, а не навпаки.

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

Кілька інших зауважень, які можуть вас направити:

  1. Тести та тестові класи повинні бути короткими та простими . Кожен тест повинен перевіряти лише цілісний функціонал. Тобто, це не хвилює речі, які інші тести вже перевіряють.
  2. Тести та ваші об'єкти повинні бути зв'язані зв'язано таким чином, що якщо ви змінюєте об'єкт, ви змінюєте лише його графік залежності вниз, а інші об'єкти, які використовують цей об'єкт, на нього не впливають.
  3. Можливо, ви створюєте та тестуєте неправильні речі . Чи створені ваші об'єкти для легкого взаємодії чи простої реалізації? Якщо це останній випадок, вам доведеться змінити багато коду, який використовує старий інтерфейс реалізації.
  4. У кращому випадку суворо дотримуйтесь принципу єдиної відповідальності. У гіршому випадку дотримуйтесь принципу роздільної інтерфейсу. Див. Принципи твердості .

5
+1 заDon't think of it as "fixing the tests", but as "defining new requirements".
StuperUser

2
+1 Тести повинні спочатку вказати вашу програму, а не навпаки
treecoder

11

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

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

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

дані жорстко закодовані.

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

assert sum([1,2,3]) == 6
assert sum([1,2,3]) == 1 + 2 + 3
assert sum([1,2,3]) == reduce(operator.add, [1,2,3])

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

дуже мало використання коду

Найкраще повторне використання коду в тестах - це imho 'Checks', як і у jUnits assertThat, оскільки вони прості тести просто. Крім того, якщо тести можуть бути відремонтовані для спільного використання коду, фактичний тестований код, ймовірно, може бути занадто , таким чином зводячи тести до тих, що тестують рефакторовану базу.


Я хотів би знати, де потік не погоджується.
кеппла

keppla - Я не знижувач, але, як правило, залежно від того, де я перебуваю в моделі, я віддаю перевагу тестуванню взаємодії об'єкта над тестуванням даних на рівні одиниці. Тестування даних працює краще на рівні інтеграції.
Річ Мелтон

@keppla У мене є клас, який спрямовує замовлення на інший канал, якщо його загальні елементи містять певні обмежені елементи. Я створюю підроблене замовлення, заповнюйте його чотирма пунктами, два з яких обмежені. Що стосується предметів із обмеженням, цей тест є унікальним. Але кроки створення підробленого замовлення та додавання двох звичайних елементів - це та сама настройка, яку використовує інший тест, який перевіряє не обмежений робочий процес. У цьому випадку, поряд із пунктами, якщо замовлення потребує встановлення даних про клієнта та налаштування адрес тощо, не є справжнім випадком повторного використання помічників налаштування. Чому тільки стверджувати повторне використання?
Асиф Шираз

6

У мене теж була ця проблема. Мій покращений підхід був таким:

  1. Не пишіть одиничні тести, якщо вони не єдиний хороший спосіб щось перевірити.

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

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

    Перш ніж хтось мене вбиває: виробництво не повинно руйнуватися за твердженнями. Натомість вони повинні увійти на рівні "Помилка".

    На застереження тому, хто ще не думав про це, не запевняйте нічого на вході користувача або мережі. Це величезна помилка ™.

    У своїх останніх базах кодів я з розумом видаляв одиничні тести, де я бачив очевидну можливість для тверджень. Це значно знизило витрати на обслуговування загалом і зробило мене значно щасливішою людиною.

  3. Віддайте перевагу тестам на систему / інтеграцію, застосовуючи їх для всіх ваших основних потоків та досвіду користувача. Тут, напевно, не потрібно бути тут. Тест системи перевіряє поведінку на кінці користувача, запускаючи всі компоненти. Через це системний тест обов'язково відбувається повільніше, тому пишіть ті, що мають значення (ні більше, ні менше), і ви знайдете найважливіші проблеми. Системні тести мають дуже низькі витрати на обслуговування.

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

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

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


Щодо питання "не пишіть одиничні тести", я наведу приклад:

TEST(exception_thrown_on_null)
{
    InternalDataStructureType sink;
    ASSERT_THROWS(sink.consumeFrom(NULL), std::logic_error);
    try {
        sink.consumeFrom(NULL);
    } catch (const std::logic_error& e) {
        ASSERT(e.what() == "You must not pass NULL as a parameter!");
    }
}

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

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

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

Тоді я мою конкретну пораду: почніть видаляти одиничні тести з розумом, коли вони ламаються, задаючи собі питання, "це вихід, чи я марную код?" Ймовірно, вам вдасться зменшити кількість речей, які витрачають ваш час.


3
Віддавайте перевагу системним / інтеграційним тестам - це незрозуміло погано. Ваша система доходить до того, що використовуючи ці (повільні!) Тести для тестування речей, які можна було швидко спіймати на рівні одиниці, і для цього потрібні години, оскільки у вас стільки подібних і повільних тестів.
Річ Мелтон

1
@RitchMelton Цілком окремо від обговорення, це здається, що вам потрібен новий сервер CI. КІ не повинен поводитись так.
Андрес Яан Так

1
Збійна програма (що і роблять твердження) не повинна вбивати вашого тестового бігуна (CI). Ось чому у вас є тестовий бігун; тож щось може виявити та повідомити про такі помилки.
Андрес Яан Так

1
Висловлювання, що знаходяться лише для налагодження, у стилі Assert, які я знайомий (не тестові твердження), спливають діалогове вікно, яке висить CI, оскільки воно чекає на взаємодію розробника.
Річ Мелтон

1
Ах, добре, що могло б пояснити багато про нашу незгоду. :) Я маю на увазі твердження у стилі С. Я лише зараз помітив, що це питання .NET. cplusplus.com/reference/clibrary/cassert/assert
Andres Jaan Tack

5

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

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

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


3

Я впевнений, що інші матимуть набагато більше вкладу, але, на мій досвід, це важливі речі, які допоможуть вам:

  1. Використовуйте фабрику тестових об'єктів для побудови структур вхідних даних, тому вам не потрібно дублювати цю логіку. Можливо, загляньте в бібліотеку помічників, таких як AutoFixture, щоб скоротити код, необхідний для установки тесту.
  2. Для кожного тестового класу централізуйте створення SUT, так що це буде легко змінити, коли все буде відновлено.
  3. Пам'ятайте, що тестовий код так само важливий, як і виробничий код. Це також має бути відновлено, якщо ви виявите, що ви повторюєтесь, якщо код відчуває себе нездійсненним тощо, тощо.

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

@driis - Правильно, тестовий код має інші ідіоми, ніж працює. Приховування речей, рефакторинг «загального» коду та використання таких предметів, як контейнери IoC, просто маскує проблеми дизайну, які піддаються вашим тестам.
Річ Мелтон

Хоча точка @pdr робить, ймовірно, дійсною для одиничних тестів, я заперечую, що для інтеграції / системних тестів може бути корисно продумати термін "підготувати додаток до завдання X". Це може включати навігацію до потрібного місця, встановлення певних налаштувань часу роботи, відкриття файлу даних тощо. Якщо кілька тестів на інтеграцію починаються в одному і тому ж місці, повторне кодування цього коду для його повторного використання в декількох тестах може бути не поганим, якщо ви розумієте ризики та обмеження такого підходу.
CVn

2

Обробіть тести, як ви робите це з вихідним кодом.

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


1

Ви обов'язково повинні ознайомитися з тестовими моделями Gerard Meszaros XUnit . У ньому є чудовий розділ з багатьма рецептами, щоб повторно використовувати тестовий код і уникнути дублювання.

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

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

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