Як ви ефективно підтримуєте тести під час переробки?


14

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

Прикладом є тестування конкретного виводу - наприклад, тексту або HTML. Тести часто (наївно?) Пишуться, щоб очікувати певного блоку тексту як виводу для деяких вхідних параметрів або для пошуку конкретних розділів у блоці.

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

  • Як ви керуєте роботою пошуку та переписування цих тестів? Що робити, якщо ви не можете просто «запустити їх усіх і дозволити рамці їх розібрати»?

  • Які ще види тестування коду призводять до нестабільних тестів?


Чим це суттєво відрізняється від programer.stackexchange.com/questions/5898/… ?
AShelly

4
Це питання помилково задали щодо рефакторингу - тести одиниць повинні бути інваріантними при рефакторингу.
Алекс Фейнман

Відповіді:


9

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

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

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

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

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


Я думаю, ви мали намір написати "поза модулем", а не "поза додатком".
СамБ

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

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

4

Я щойно завершив капітальний ремонт мого стеку SIP, переписавши весь транспорт TCP. (Це був майже рефактор, досить масштабний, відносно більшості рефакторинга.)

Якщо коротко, є TIdSipTcpTransport, підклас TIdSipTransport. Всі TIdSipTransports мають спільний тестовий набір. Внутрішніми для TIdSipTcpTransport були ряд класів - карта, що містить пари з'єднань / ініціюючих повідомлень, потокові клієнти TCP, потоковий TCP-сервер тощо.

Ось що я зробив:

  • Видалено класи, які я збирався замінити.
  • Видалено тестові набори для цих класів.
  • Залишив тестовий набір, характерний для TIdSipTcpTransport (і все ще був тестовий набір, спільний для всіх TIdSipTransports).
  • Пройдіть тести TIdSipTransport / TIdSipTcpTransport, щоб переконатися, що всі вони не вдалися.
  • Прокоментував усі, крім одного тесту TIdSipTransport / TIdSipTcpTransport.
  • Якщо мені потрібно було додати клас, я додав би тести для написання, щоб створити достатню кількість функціональних можливостей, щоб пройшов єдиний тест без коментарів.
  • Натріть, промийте, повторіть.

Таким чином, я знав, що мені ще потрібно зробити, у формі тестованих коментарів (*), і знав, що новий код працює, як очікувалося, завдяки новим тестам, які я написав.

(*) Дійсно, вам не потрібно їх коментувати. Просто не запускайте їх; 100 провальних тестів не дуже обнадійливі. Крім того, у моїй конкретній програмі компіляція меншої кількості тестів означає швидший цикл тесту-запису-рефактора.


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

3

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

Ти можеш:

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

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


Це також може допомогти використовувати структурний HTML.
SamB

@SamB, безумовно, це допоможе, але я не думаю, що це повністю вирішить проблему
Вінстон Еверт,

Звичайно, ні, нічого не може :-)
SamB

-1

Спершу створіть НОВИЙ API, який виконує те, що ви хочете, щоб було ваше поведінка NEW API. Якщо трапляється, що цей новий API має те саме ім'я, що і API OLDER, я додаю ім'я _NEW до нового імені API.

int DoSomethingInterestingAPI ();

стає:

int DoSomethingInterestingAPI_NEW (int takes_more_arguments); int DoSomethingInterestingAPI_OLD (); int DoSomethingInterestingAPI () {DoSomethingInterestingAPI_NEW (what_default_mimics_the_old_API); Гаразд - на цьому етапі всі ваші регресійні тести проходять підвіконня - використовуючи назву DoSomethingInterestingAPI ().

НАСТУПНО, перейдіть через свій код і змініть всі дзвінки на DoSomethingInterestingAPI () на відповідний варіант DoSomethingInterestingAPI_NEW (). Це включає оновлення / перезапис будь-яких частин регресійних тестів, які потрібно змінити, щоб використовувати новий API.

NEXT, позначте DoSomethingInterestingAPI_OLD () як [[застаріле ()]]. Тримайте застарілий API, скільки завгодно (поки ви безпечно не оновите весь код, який може від нього залежати).

При такому підході будь-які збої у ваших тестах регресії просто є помилками в цьому регресійному тесті або виявлення помилок у вашому коді - саме так, як ви хотіли. Цей поетапний процес перегляду API шляхом явного створення _NEW та _OLD версій API дозволяє деякий час співіснувати біти нового та старого коду.

Ось хороший (важкий) приклад такого підходу на практиці. У мене була функція BitSubstring () - там, де я застосував підхід третього параметра, бути COUNT біт у підрядку. Щоб відповідати іншим API та шаблонам у C ++, я хотів переключитися на початок / кінець як аргументи функції.

https://github.com/SophistSolutions/Stroika/commit/003dd8707405c43e735ca71116c773b108c217c0

Я створив функцію BitSubstring_NEW з новим API і оновив весь свій код, щоб використовувати це (залишаючи НЕ БІЛЬШЕ ЗВ'ЯЗКУ для BitSubString). Але я залишив у реалізації кілька випусків (місяців) - і позначив її застарілою - щоб кожен міг перейти на BitSubString_NEW (і в цей час змінити аргумент із стипу підрахунку на початок / кінець).

ТОГО - коли цей перехід був завершений, я здійснив черговий комітет, видаляючи BitSubString () та перейменувавши BitSubString_NEW-> BitSubString () (і застаріле ім'я BitSubString_NEW).


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

Ви повністю пропустили пункт. По-перше - це не суфікси, які "не мають ніякого значення". Вони несуть в собі значення того, що API переходить від старого до більш нового. Насправді, у цьому вся суть ПИТАННЯ, на яке я відповідала, і вся суть відповіді. Імена ЧИСЛО повідомляють, що це OLD API, який є NEW API, і який в кінцевому підсумку є цільовим ім'ям API, коли перехід буде завершений. І - суфікси _OLD / _NEW тимчасові - ТІЛЬКИ під час переходу зміни API.
Льюїс Прінгл

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