Визначення модульного тесту
У наших з вами інтернетах нещодавно піднявся неабиякий шум з приводу того, чи живий TDD зараз, чи потрібен він, і в якому вигляді. Все почалося зі статті Девіда Хансона "TDD is dead. Long living testing ", після якої послідували статті багатьох авторів і живі обговорення цієї проблеми включаючи hangout разом з Девідом, Кентом Беком і Мартіном Фаулером (до речі, черговий hangout буде завтра, 16 травня).
Але деякі знають, що за кілька днів до цього все той же Мартін Фаулер постарався дати визначення модульного тесту (bliki:UnitTest), переклад якого представлений нижче. А після перекладу йдуть деякі мої думки з цього приводу.
—
У світі розробки ПЗ дуже часто говорять про модульні тести, і я знайомий з цим поняттям протягом всієї своєї кар'єри програміста. Однак, як і багато інших термінів зі світу розробки ПЗ, цей термін визначений досить погано, і я часто стикаюся із замішанням, коли розробники думають, що він має більш суворе визначення, ніж це є насправді.
І хоча я дуже часто користувався модульними тестами, моя остаточна прихильність ними виникла, коли я почав працювати з Кентом Беком і користуватися сімейством інструментів тестування xUnit. (Мені навіть іноді здається, що більш підходящим терміном для цього виду тестування буде «xunit testing».) Модульне тестування також стало важливою активністю в Екстремальному Програмуванні (XP - Extreme Programming), і швидко переросло в розробку через тестування (TDD - Test-Driven Development).
Роль модульних тестів в XP з самого початку викликала певне занепокоєння. Я чітко пам'ятаю обговорення в групі usenet, в якій експерт з тестування лаяв прихильників XP за неправильне використання терміну «модульний тест». Ми попросили дати його визначення, на що він відповів щось на зразок: «на самому початку мого навчального курсу з тестування я розглядаю 24 різних визначення модульного тесту».
Незважаючи на розбіжності, в деяких аспектах наші точки зору сходилися. По-перше, існує уявлення, що модульні тести є низькорівневими і концентруються лише на невеликій частині програмної системи. По-друге, сьогодні модульні тести зазвичай пишуться розробниками з використанням їх звичайних інструментів, до яких додається деякий фреймворк для тестування (*). По-третє, очікується, що модульні тести будуть суттєво швидшими за інші види тестів.
Але були й відмінності в поглядах. Існує різні точки зору на те, що вважати модулем. В об'єктно-орієнтованому дизайні модулем прийнято вважати клас, в процедурному і функціональному підходах модулем може вважатися функція. Насправді ж, це ситуативне поняття: команда вирішує, що розумно вважати модулем для розуміння системи або її тестування. І хоча я починаю з уявлення, що клас є модулем, я часто починаю розглядати набір тісно пов'язаних класів, як єдиний модуль. Рідше я можу розглядати підмножину методів класу в якості модуля. Насправді, не має особливого значення, як ви визначите це поняття.
Ізоляція
Більш важливою відмінністю в підходах є питання: чи повинен тестований модуль бути відокремлений від взаємодіючих об'єктів? Припустимо ви тестуєте метод обчислення ціни класу замовлення. Метод обчислення ціни викликає деякі методи класів продукту і замовника. Якщо ви дотримуєтеся принципу ізоляції взаємодіючих об'єктів, ви не захочете тут використовувати реальні класи продуктів і замовників, оскільки помилки в класі замовника призведуть до падіння тестів класу замовлення. Замість цього ви скористаєтеся підробками (Test Doubles) всіх взаємодіючих об'єктів.
Але не всі розробники використовують ізоляцію. Насправді, коли xunit-тестування почалося в 90-х ми не намагалися ізолювати тестований клас, якщо тільки комунікація з іншими об'єктами не була вкрай незручною (як, наприклад, взаємодія з віддаленою системою перевірки кредитних карток). У нас не виникало складнощів зрозуміти реальну причину збою, навіть якщо при цьому падали сусідні тести. Тому, з практичної точки зреня, ми не вважали відсутність ізоляції проблемою.
Хоча саме відсутність ізоляції в нашому визначенні «модульного тесту» була причиною його критики. Я вважаю, визначення «модульного тесту» підходящим, оскільки він тестує поведінку одного модуля. Ми пишемо тест, припускаючи, що все, крім даного модуля працює коректно.
Коли xunit тестування стало набирати популярність в 2000-і, ідея ізоляції повернулася з новою силою, принаймні, для деяких. Ми бачили підйом Мок Об'єктів (Mock Object) і фреймворків для підтримки мокінгу. У результаті з'явилися дві школи xunit тестувальників, які я називаю класичною школою і школою мокістів (mockists). Прихильники класичної школи не заморочуються з ізоляцією, як це роблять маркісти. Я знаю і поважаю xunit-тестувальників обох шкіл (хоча сам ставлюся до класичної школи).
Навіть представники класичної школи (включаючи мене) при наявності складних взаємодій використовують підробки (test doubles). Підробки є безцінними для усунення невизначеності поведінки при роботі з віддаленими сервісами. Деякі представники класичної школи вважають, що будь-яка взаємодія із зовнішніми ресурсами, такими як бази даних або файлова система, повинні використовувати підробки. Частково ця думка спирається на ризик невизначеної поведінки, частково на проблеми зі швидкістю. І хоча я вважаю, що це корисна рекомендація, я не розглядаю її як абсолютне правило. Якщо звернення до ресурсу є стабільним і досить швидким для вас, тоді немає причин, чому його не можна використовувати з модульних тестів.
Швидкість
Існують кілька загальних властивостей юніт тестів: маленька область дії (small scope), вони пишуться розробниками, і вони швидко виконуються - що дає можливість запускати їх часто під час розробки. Дійсно, це одна з ключових властивостей самотестируемого коду (Self-Testing Code). У цьому випадку програміст може запускати модульні тести після будь-якої зміни в коді. Я можу запускати юніт тести кілька разів на хвилину, кожен раз, коли у мене з'являється необхідність в компіляції коду. Це корисно, оскільки якщо я випадково щось зламаю, то я хочу відразу ж дізнатися про це. Якщо я зламав щось своїми останніми змінами, то набагато простіше відразу ж знайти цю помилку, оскільки мені не доведеться шукати її дуже далеко.
ПРИМІТКА перекладача
Кент Бек розвинув ідею запуску тестів при компіляції (а іноді навіть без неї) і запропонував ідею безперервного тестування (Continuous Testing). Приклади таких інструментів: Mighty-Moose і NCrunch для .NET, JUnit Max для Java.
Коли ви запускаєте тести настільки часто, то ви не можете запускати їх всі. Зазвичай вам потрібно запускати лише ті тести, які працюють з кодом над яким ви зараз працюєте. У цьому випадку ви жертвуєте глибиною тестування на догоду тривалості запуску тестів. Я називаю цей набір тестів «набором компіляції» (compile suite), оскільки я запускаю їх кожен раз при компіляції, навіть на таких інтерпретованих мовах як Ruby.
Якщо ви використовуєте безперервну інтеграцію (Continuous Integration), ви повинні запускати тести, як один з її кроків. Цей набір тестів, які я називаю «набором фіксації» (commit suite), повинен включати всі юніт тести. Він також може включати в себе деякі приймальні тести (Broad-Stack Tests або End-to-End Tests). Як розробник ви повинні проганяти цей набір тестів кілька разів на день, звичайно ж, до фіксації своїх змін у системі контролю версій, а також у будь-який інший час, коли у вас є така можливість - під час перерви або мітингу. Чим швидше виконується набір тестів фіксації, тим частіше ви зможете їх запускати (* *).
У різних людей різні стандарти для швидкості виконання юніт-тестів та їх наборів. Так, для Девіда Хансона (David Heinemeier Hansson) достатньо, щоб набір компіляції (compile suite) виконувався кілька секунд, а набір фіксації (commit suite) - кілька хвилин. Гаррі Бернхардт (Gary Bernhardt) вважає це занадто повільним і наполягає, щоб набір компіляції виконувався близько 300мс, а Ден Бодарт (Dan Bodart) не хоче чекати виконання набору фіксації довше декількох секунд.
Я не думаю, що є єдина правильна відповідь на це питання. Особисто я не бачив різниці, коли набір компіляції виконується частку секунди або пару секунд. Мені подобається правило Кента Бека, що набір фіксації не повинен виконуватися довше 10 хвилин. Головна думка тут в тому, що ваш набір тестів повинен виконуватися досить швидко, щоб не відбити у вас полювання запускати його досить часто. А «досить часто» означає, що коли тести знайдуть, вам доведеться перекопати невеликий обсяг коду і знайти його досить швидко.
Примітки
(*) Я кажу «сьогодні», оскільки це змінилося саме завдяки XP. У суперечках початку нового століття, прихильники XP піддавалися серйозній критиці, оскільки загальноприйнята точка зору гласила, що програмісти не повинні тестувати власний код. У деяких компанія були спеціалізовані «юніт-тестери», єдиним завданням яких було написання модульних тестів для коду розробників. Причина такої точки зору полягала в наступному: люди володіють «концептуальною сліпотою» при тестуванні свого коду; програмісти є поганими тестувальниками, тому корисно мати якусь форму протистояння між програмістами та тестувальниками. Точка зору прихильників XP полягала в тому, що програмісти можуть навчитися бути хорошими тестувальниками, як мінімум на рівні окремого «модуля», а якщо залучити додаткову групу для написання тестів, то зворотний зв'язок, що забезпечується тестами, буде неймовірно повільним. Інструменти XUnit грали в цьому дуже важливу роль, оскільки вони були розроблені спеціально, для мінімізації накладних витрат при написанні тестів.
(* *) Якщо у вас є корисні тести, тривалість виконання яких перевершує тривалість запуску тестів фіксації, то вам потрібно побудувати «конвеєр розгортання» (Deployment Pipeline) і помістити ці тести в більш пізні етапи конвеєра.
—
У цій статті Мартін усвідомлено не стосується питань порядку написання коду і тестів, замість цього він намагається лише дати визначення модульного тесту і показати існування різних точок зору на саме поняття модуля, на необхідність ізоляції і швидкість виконання.
Сам я теж досить часто стикався з думкою, що модульний тест повинен тестувати клас у повній ізоляції від решти світу. Наприклад, саме цей підхід описує Роберт Мартін у своїй книзі «Принципи, патерни і методики гнучкої розробки» і саме його я критикував у статті «Критичний погляд на принцип інверсії залежностей».
У моєму розумінні немає абсолютно нічого поганого у використанні конкретних класів в модульних тестах, якщо їх поведінка є детермінованою і швидкою. Виділення зайвих залежностей може підривати інкапсуляцію класу і в підсумку знижувати простоту розуміння і супроводу системи. Будь-які стабільні залежності можуть і повинні використовуватися безпосередньо, а виділятися повинні лише «мінливі» залежності, чия поведінка не є детермінованою.
Виявляється, я є прихильником класичної школи модульного тестування, і вважаю, що потрібно використовувати реальні класи, якщо вони не звертаються до недермінованих зовнішніх ресурсів. Проблема з великою кількістю моків у моєму розумінні полягає в тому, що отримані тести стають занадто залежними на тестове оточення, що робить їх крихкими, а велика кількість інтерфейсів і непрямості погіршує «розумність» системи додаючи гнучкість, не потрібну в 99% випадків.
Звичайно, є й інші точки зору на використання моків. Так, Стів Фріман і Нет Прайс у своїй книзі «Growing Object-Oriented Software Guided by Tests» дотримуються іншої точки зору. Але при цьому вони дуже ретельно стежать за простою тестів і не допускають ситуації, коли на кожен рядок тесту приходить 5 рядків ініціалізації моків.
Абсолютно нормально дотримуватися будь-якому з двох таборів: класиків або моккістів. Головне, щоб ваш вибір був усвідомленим, а ваші тести спрощували розвиток і супровід, а не заважали цьому.



