- Серія контенту:
- Цей контент є частиною серії: Практичне використання Rails
- Засоби тестування Rails
- модульні тести
- Лістинг 1: Тест основної моделі
- Лістинг 2. Helper-метод для тестування робочих асоціацій
- Функціональне та комплексне тестування
- Лістинг 3. Простий функціональний тест
- заглушки
- Лістинг 4. Заглушка для входу в систему
- Основні поняття
- тестове покриття
- Лістинг 5. Запуск rake test: coverage з RCov
- Малюнок 1. Реальний звіт RCov
- швидкість
- Фіктивні об'єкти і заглушки в Mocha і FlexMock
- Лістинг 6. Створення простої заглушки
- Лістинг 7. Бібліотека фіктивних об'єктів Mocha
- безперервна інтеграція
- Висновок
- Ресурси для скачування
Практичне використання Rails
Вибір оптимальних інструментальних засобів і технологій для команди Rails-програмістів
Серія контенту:
Цей контент є частиною # з серії # статей: Практичне використання Rails
https://www.ibm.com/developerworks/ru/views/global/libraryview.jsp?series_title_by=Практическое+использование+rails
Слідкуйте за виходом нових статей цієї серії.
Цей контент є частиною серії: Практичне використання Rails
Слідкуйте за виходом нових статей цієї серії.
Основною відмінною особливістю платформи Rails є сама мова Ruby. Як і будь-яка мова програмування з динамічною типізацією, Ruby володіє достатньою гнучкістю, зручністю використання і хорошою продуктивністю. Але все має свою ціну. У мовах програмування з динамічною типізацією немає компілятора, який би відстежував певні види помилок, в тому числі досить поширені помилки типів і орфографічні помилки. Користувачі об'єктно-орієнтованих мов програмування з динамічною типізацією дуже швидко зрозуміли, що їм необхідно тестувати свої програми.
У співтоваристві Ruby on Rails до тестування ставляться так само, як в США - до телешоу American Idol. Мої друзі регулярно стежать за результатами тестів. Розробники Ruby багато говорять про тестування, пишуть про нього в своїх блогах і навіть ведуть діяльність в офлайні: звичайно ж, вони не голосують з мобільних телефонів, а беруть участь у створенні середовищ з відкритим вихідним кодом.
Якби тестування не виконувалося, то в Ruby-додатках було б набагато більше помилок на одну сходинку коду. Тестування дозволяє використовувати всі переваги мов програмування з динамічною типізацією і мінімізувати їх недоліки. У даній статті не розглядаються такі загальні питання, як "чи потрібно виконувати тестування чи ні", або "як переконати керівництво, що зусилля, витрачені на тестування, виправдані". Будемо вважати, що ви вже використовуєте тестування. Замість цього будуть проаналізовані деякі більш тонкі рішення, до яких, в кінцевому рахунку, повинен вдатися кожен керівник проекту Ruby. Ми поговоримо про те, як можна виміряти тестове покриття і яка кількість тестів необхідно виконувати. У нашій статті буде проведено детальний аналіз основних методик тестування і представлено їх порівняння з новітніми mock-середовищами (mocking frameworks). Дана стаття не є покроковим керівництвом - в ній наведено кілька прикладів методик тестування, які застосовувалися для створення сайту ChangingThePresent.org (див. Розділ ресурси ), І у вас є можливість побачити їх у дії. Крім того, в статті будуть показані сильні і слабкі сторони різних методик тестування.
Засоби тестування Rails
У середу Rails вбудована напрочуд надійна і готова до використання система тестування. Витрачаючи мінімум зусиль, можна задавати відтворювані настройки баз даних, відправляти Web-додатків тестові HTTP-повідомлення та виконувати три види тестування: модульне, функціональне і комплексне. У наступному розділі наведені короткі приклади всіх видів тестування.
модульні тести
Модульні тести перевіряють код моделі Rails і іноді helper-методи. Модульні тести дозволяють переконатися, що модель виконує те, для чого вона була створена, і що асоціації в моделі ведуть себе так, як і передбачалося. Ви вже знаєте, що моделі Rails є об'єктами, які працюють тільки з однією таблицею бази даних. У більшості випадків кожен стовпець бази даних є атрибутом моделі. Helper-методи Rails є функціями, які допомагають спростити код моделі, уявлення або контролера. Необхідно переконатися, що для кожної моделі або helper-методу є тест. У проекті ChangingThePresent модульні тести для більшості основних моделей дуже невеликі.
Лістинг 1: Тест основної моделі
require File.dirname (__ FILE__) + '/../test_helper' class BannerStyleTest <Test :: Unit :: TestCase fixtures: banner_styles def test_associations assert_working_associations end def test_validation_with_incorrect_specs_should_fail bs = BannerStyle.new (: height => 10,: width => 10,: format => 'vertical_rectangle',: model_name => 'Nonprofit') assert! bs.save, bs.errors.inspect bs2 = BannerStyle.new (: height => 400,: width => 240,: format = > 'vertical_rectangle',: model_name => 'Nonprofit') assert bs2.save, bs2.errors.inspect end ... end
У лістингу 1 показаний невеликий набір тестових даних з двома тестами. Підпрограма BannerStyle створює прості рекламні банери. Розміри і форми кожного банера залежать від стандартів. Щоб забезпечити відповідність кожного нового банера необхідним стандартам, в додатку використовується таблиця стандартів. У першому тесті helper-метод використовується для перевірки всіх асоціацій в BannerStyle за допомогою відображення, як показано в лістингу 2. Другий тест не дозволяє зберігати банери з некоректними значеннями висоти і ширини і забезпечує правильне збереження моделлю банерів з вірними характеристиками.
Лістинг 2. Helper-метод для тестування робочих асоціацій
def assert_working_associations (m = nil) m || = self.class.to_s.sub (/ Test $ /, '') .constantize @m = m.new m.reflect_on_all_associations.each do | assoc | assert_nothing_raised ( "# {assoc.name} caused an error") do @ m.send (assoc.name, true) end end true end
У лістингу 2 показаний helper-метод, який перевіряє всі асоціації класу. Метод assert_working_associations просто перебирає всі асоціації класу і відправляє в модель назва асоціації. Такий підхід гарантує, що за допомогою одного рядка коду в моделі можна активізувати всі зв'язки у всіх тестах моделі.
Функціональне та комплексне тестування
Функціональні тести перевіряють роботу призначеного для користувача інтерфейсу за допомогою окремих HTTP-запитів. Середа Rails дозволяє легко ініціювати окремі команди HTTP GET і POST, формуючи основу тестів. Комплексні тести аналогічні функціональним тестам, але вони дозволяють ініціювати безліч каскадних HTTP-запитів. Принцип і структура тестів ті ж самі. У лістингу 3 показано кілька простих функціональних тестів.
Лістинг 3. Простий функціональний тест
require File.dirname (__ FILE__) + '/../test_helper' require 'causes_controller' class CausesController; def rescue_action (e) raise e end; end class CausesControllerTest <Test :: Unit :: TestCase fixtures: causes,: members,: quotes,: cause_images,: blogs,: blog_memberships def setup @controller = CausesController.new @request = ActionController :: TestRequest.new @response = ActionController :: TestResponse.new end def test_index get: index assert_response: success assert_template 'index' assert_not_nil assigns (: causes) assert_equal Cause.find_all_ordered.size, assigns (: causes) .size end def test_should_create_blog assert Cause.find (2) .blog .nil? get: create_blog,: id => 2 assert Cause.find (2) .blog.nil? login_as: bruce get: create_blog,: id => 2 assert! Cause.find (2) .blog.nil? assert_equal Cause.find (2) .name, Cause.find (2) .blog.title end
З лістингу 3 видно, що всі взаємодії між тестом і системою виконуються за допомогою HTTP-методів GET і POST. Основний алгоритм тестування виглядає наступним чином:
- Запуск простий HTTP-операції.
- Перевірка впливу HTTP-операції на систему.
Більш того, в лістингу 3 метод setup задає фіксовані значення для моделювання HTTP-запитів. З їх допомогою усуваються всі вимоги до мережі та інфраструктури, тим самим набори тестових даних ізолюються рамками самого додатка.
заглушки
У проекті ChangingThePresent.org ми додали кілька тестових helper-методів, які спростили, наприклад, процедуру реєстрації в системі. У лістингу 3, в п'ятому рядку методу test_should_create_blog показаний виклик методу login_as: bruce. Даний код запускає helper-метод, показаний в лістингу 4, який замінює процедуру реєстрації входу на сайт, копіюючи ID користувача безпосередньо в сесію. Якщо ви використовували плагін Rails acts_as_authenticated, то знаєте, що ввійшов в систему користувачеві присвоюється значення, пов'язане з ключем: user сесії.
Лістинг 4. Заглушка для входу в систему
def login_as (member) @ request.session [: user] = member? members (member) .id: nil end
Більшість розробників плутають поняття заглушка (stub) і фіктивний об'єкт (mock). Заглушка просто замінює реальне втілення його спрощеної реалізацією. У лістингу 4 заглушка замінила всю систему реєстрації на її спрощену імітацію. Заглушка імітує реальний стан речей. Mock-об'кти не є заглушками. Mock-об'єкт схожий на датчик, який вимірює, яким чином додаток використовує інтерфейс. Далі буде наведено більш детальний опис заглушок і показано кілька прикладів.
Основні поняття
Тепер ви знаєте, як використовувати вбудовані засоби тестування Rails. Перш ніж йти далі, хотілося б позначити пару ключових проблем: обсяг і швидкість тестування. Раз ви виробляєте загальну філософію тестування, вам буде потрібно звернути увагу на знаходження компромісу між швидкістю і покриттям тесту.
тестове покриття
На даному етапі необхідно прийняти одне з найважливіших рішень - в якому обсязі необхідно проводити тестування. Якщо виконати недостатня кількість тестів, це може позначитися на якості коду і, ймовірно, навіть призведе до затримки поставки готового ПО. З іншого боку, можна виконати і занадто велика кількість тестів. Якщо проводити тестування занадто довго, є ймовірність перевищення термінів поставки готового ПО, що вкрай небажано в умовах бізнесу. Щоб прийняти зважене і обдумане рішення щодо кількості виконуваних тестів, слід ретельно виміряти, який їх обсяг вже виконується. Одним з найважливіших чисельних показників, пов'язаних з тестуванням, є покриття коду.
У проекті ChangingThePresent для визначення тестового покриття використовується RCov. Можна запустити стандартну команду rake і в якості звіту отримати звичайний набір точок. Також можна запустити rake test: coverage для отримання більш повного і докладного звіту, наприклад такого, який показаний в лістингу 5.
Лістинг 5. Запуск rake test: coverage з RCov
807 tests, 2989 assertions, 0 failures, 0 errors + -------------------------------------- -------------- + ------- + ------- + -------- + | File | Lines | LOC | COV | + ------------------------------------------------- --- + ------- + ------- + -------- + | app / controllers / address_book_controller.rb | 142 | 123 | 84.6% | | App / controllers / admin_controller.rb | 77 | 65 | 93.8% | | App / controllers / advisor_admin_controller.rb | 86 | 63 | 88.9% | | App / controllers / advisors_controller.rb | 52 | 42 | 100.0% | ... | app / models / stupid_gift.rb | 56 | 45 | 100.0% | | App / models / stupid_gift_image.rb | 10 | 10 | 100.0% | | App / models / titled_comment.rb | 2 | 2 | 100.0% | | App / models / upgrade.rb | 13 | 10 | 100.0% | | App / models / upgrade_item.rb | 3 | 3 | 100.0% | | App / models / validation_model.rb | 7 | 7 | 100.0% | | App / models / volunteer_opportunity.rb | 137 | 129 | 93.0% | | App / models / work_period.rb | 5 | 4 | 100.0% | + ------------------------------------------------- --- + ------- + ------- + -------- + | Total | 12044 | 10044 | 81.8% | + ------------------------------------------------- --- + ------- + ------- + -------- + 81.8% 167 file (s) 12044 Lines 10044 LOC
Виконання RCov займає набагато більше часу (приблизно в два рази в порівнянні з часом, необхідними на ведення протоколу наших тестів), тому я не користуюся цією командою постійно. Однак запустивши її, можна буде точно дізнатися значення тестового покриття в будь-якому файлі. І, що ще краще, можна відкрити файл покриття в вікні браузера і подивитися, які рядки коду вже охоплені тестами. На малюнку 1 показаний приклад типового звіту про покриття.
Малюнок 1. Реальний звіт RCov
Тепер, коли у нас є чисельні дані, можна почати робити приблизні оцінки необхідного обсягу тестів. При тестуванні сайту ChangingThePresent статистичні дані по тестовому покриттю варіювалися, але в підсумку ми прийшли до значення 80 - 85%. Так як на стадії розробки знаходяться нові важливі функції, тестове покриття буде тимчасово зменшено. Як тільки ці функції стануть доступними користувачам по мережі, тестове покриття збільшиться. На даний момент значення тестового покриття одно 81,7%.
Майте на увазі, що отримані нами результати можуть в результаті відрізнятися від ваших. Повнота тестового покриття залежить від досвідченості розробників, складності додатка, критичності наявності помилок для роботи програми та допустимості затримки термінів здачі додатки з точки зору бізнесу. Якщо розробляється додаток для проектування літаків, знадобиться більше тестів, а якщо ви заради власного задоволення створюєте додаток Web 2.0 для Facebook, яке через два місяці буде нікому не потрібно, якщо випадково не потрапити в незайняту ринкову нішу, то обсяг тестування буде набагато менше. Кращі з відомих мені Ruby-програмістів досягають покриття вихідного коду понад 80%, а деякі прагнуть домогтися 100% покриття.
Навіть якщо вдасться домогтися 100% покриття, у вас все одно не буде ніяких гарантій якості самих тестів. Щоб домогтися максимально можливого тестового покриття, необхідно виконувати різні типи тестів, що моделюють як стандартний хід виконання операцій (happy paths), так і граничні умови.
Тепер, коли у вас є інструментальні засоби для оцінки необхідного обсягу тестування, можна перейти до обговорення швидкості тестування. У Rails швидкість тестування визначається базою даних. Традиційні спроби проведення тестування за допомогою інструментальних засобів на основі баз даних пов'язані з проблемами. Найбільш значимими проблемами є повторюваність і швидкість. З точки зору повторюваності, вкрай складно створити хороший набір тестів без зміни бази даних. Справа в тому, що зміна бази даних призводить до зміни тестових даних, яке в свою чергу впливає на поведінку самих тестів. А друга проблема - це швидкість. Зміна бази даних - досить дороге задоволення.
швидкість
Як відомо, середа Rails вирішує проблему повторюваності за допомогою фіксованих величин. Кожен розробник задає фіксовані величини для тестових даних. Перед виконанням кожного набору тестових даних середу тестування Rails буде повністю прати дані всіх моделей і завантажувати всі фіксовані величини, які були задані для кожного набору тестових даних. Тепер кожен набір тестових даних може починатися з нульового стану. Однак в кожному наборі тестових даних міститься кілька окремих тестів, кожен з яких повинен бути абсолютно незалежним від всіх інших. Завантаження цілого набору фіксованих величин для кожного окремого тесту буде проходити дуже повільно.
Rails частково вирішує проблему швидкості, приходячи до блискучого компромісу. Після виконання кожного контрольного прикладу Rails відкочується все зміни, зроблені в базі даних. Відкат працює набагато швидше, ніж завантаження всіх фіксованих величин з нуля. Однак витрати на доступ до бази даних очевидні. Навіть з використанням відкатів тестування на основі бази даних все одно залишається повільним. А якщо тести працюють повільно, то розробники просто не будуть їх запускати. А якщо тести не запускають, то вони абсолютно марні. Хоча Rails впоралася з вирішенням проблеми повторюваності, проблема швидкості і раніше вирішена не повністю. Швидкість виконання тестування буде впливати на розвиток стратегій тестування в найближчі роки.
Одним з альтернативних підходів є використання для тестування бази даних, розміщеної в оперативній пам'яті. Як правило, SQLite працює набагато швидше, ніж MySQL. З іншого боку, тестування може виконуватися на інших платформах, окрім платформи вашої виробничої системи.
Якщо для підтримки бази даних використовується ActiveRecord, ймовірно, що модульне тестування буде виконуватися на основі бази даних, а низька швидкість компенсується витратами на розробку. Але ніщо не змушує використовувати для функціональних тестів моделі на основі баз даних. В даний час багато розробників Rails використовують заглушки (stubs) або фіктивні об'єкти (mocks), щоб не вдаватися до бази даних, створюючи тим самим функціональні тести з особливо високою швидкодією.
Фіктивні об'єкти і заглушки в Mocha і FlexMock
Раніше я розповідав, що заглушка просто замінює реальне втілення спрощеної реалізацією. У наборах тестових даних заглушки можуть використовуватися для того, щоб спростити і прискорити реалізацію і зробити її більш передбачуваною. Наприклад, може знадобитися, щоб системний годинник весь час видавали один і той же час, щоб мати можливість отримати повторювані результати тесту і перевірити їх.
Середа Mocha спрощує використання заглушок. Досить вказати, який результат ви б хотіли отримати. У лістингу 6 показаний код, який буде змушувати системний клас Date завжди повертати одну і ту ж дату, наприклад, День бабака.
Лістинг 6. Створення простої заглушки
ground_hog_day = Date.new (2007, 2, 2) Date.stubs (: today) .returns (ground_hog_day) assert_equal 2, Date.today.day
Якщо заглушка реалізує спрощену модель реального світу, то фіктивний об'єкт (mock) робить набагато більше. Іноді простий імітації реального світу може не вистачити. При виконанні тестування буває необхідно переконатися, що код програми коректно використовує API-функції. Наприклад, може знадобитися перевірити, що СУБД-додаток встановило з'єднання з БД, виконало запит і розірвало з'єднання; або перевірити, що контролер дійсно викликає метод save для об'єкта моделі. Таким чином, фіктивний об'єкт (mock object) повинен визначати очікування, а також поведінку.
У Rails присутній, щонайменш, три бібліотеки фіктівніх об'єктів: Mocha, FlexMock и RSpec. Більш докладно я розповім про Mocha, однак у кожної бібліотеки є свои Преимущества. При використанні бібліотеки Mocha фактично відбувається перерахування кожного очікуваного звернення до API з подальшою вказівкою результатів, які Mocha повинна повернути, як показано в лістингу 7.
Лістинг 7. Бібліотека фіктивних об'єктів Mocha
mock_member = mock ( "member_67") mock_member.expects (: valid?). returns (true) mock_member.expects (: save) .returns (true) mock_member.expects (: valid_captcha =). with (true) mock_member.expects ( : plaintext_password) .returns ( 'password') mock_member.expects (: id) .returns (67) Member.expects (: find_by_login) .with (nil) .returns (mock_member) post: create,: member => {}, : nonprofit => {: id => 67} ... assert_response: redirect assert_redirected_to: controller => 'nonprofits',: action => 'show',: id => mock_nonprofit_id
У лістингу 7 показаний приклад набору тестових даних для створення нового учасника. Можна задати очікування для кожного взаємодії між контролером і користувачем-заглушкою. Створення учасника-заглушки (mock member) і визначення взаємодій не залежать одне від одного. Потім я фактично створю заглушку для класу Member і заглушку для визначника, що повертає mock_member.
Очевидно, що в деякій мірі має місце взаємодія з рівнем моделі, однак mock-об'єкт повністю ізолює поведінку учасника від набору даних функціонального тесту. В даному випадку є пара очевидних переваг. Використовувати деякі API, наприклад, перевірки кредитної картки або "кнопки самоліквідації", недоцільно. Інші API, наприклад, служби на основі часу або пам'яті, недостатньо передбачувані. Щоб замінити їх заглушками або mock-об'єктами доведеться практично завжди використовувати середовища mock-об'єктів.
Більший інтерес представляє питання, чи слід використовувати заглушки і фіктивні об'єкти для моделі, заснованої на базі даних. Однією з переваг є швидкість: даний набір тестових даних ніяк не відіб'ється на базі даних. Ще одна перевага - незалежність. Я повністю ізолював тестований код від рівня контролера. Але, можливо, ви також знайдете і недоліки. Набори тестових даних були значно ускладнені. Крім того, зміна поведінки моїх моделей викликає ланцюгову реакцію, так як необхідно змінити об'єкт моделі та набори тестових даних, які їх оточують. Якщо упущено щось важливе, внести зміни в набір тестових даних не складає ніяких труднощів. Додавання однієї єдиної валідації може порушити весь сценарій і залишитися непоміченим. Тому в проекті ChangingThePresent класи об'єктів моделі не замінюються фіктивними класами. Ми обмежуємо використання фіктивних об'єктів тільки рамками зовнішніх інтерфейсів, наприклад, Web-сервісами для третіх осіб або мережевими сервісами.
Необхідно підкреслити, що підприємницькі кола Ruby схиляється до стратегій використання фіктивних об'єктів. Моя команда йде проти течії і однозначно виступає за те, щоб продовжувати використовувати технології на основі баз даних. Ми спробували і те, і інше. Ми використовуємо дані технології, так як вони більшою мірою нам підходять і краще поєднуються з використовуваним нами кодом.
безперервна інтеграція
Найбільш важливою зміною, яке ми додали в підсумкову процедуру, є безперервна інтеграція (Continuous integration - CI). Ми запустили версію Cruise Control для Ruby. Наш сервер CI перевіряє правильність складання і запускає набори тестових даних з нуля кожного разу при входженні нового зміни. Сервер сповіщає кожного розробника щоразу, коли зміна порушує процес складання. Сервер дозволяє запускати кілька репрезентативних тестів перед реєстрацією. Можна змінити лише кілька рядків в Member, а потім запустити unit / member_test.rb і functional / members_controller_test.rb. Через п'ятнадцять секунд я зможу виконати реєстрацію, будучи впевненим, що Cruise Control сповістить мене, якщо є будь-які помилки.
Висновок
Вельми цікаві суперечки, які кілька років розгорталися навколо тестування, оберталися навколо наступного питання: чи хороші автоматизовані тести? В даний час дебати стали набагато цікавіше:
- Чи повинні всі тести бути реалізовані на основі баз даних або для локалізації функціональних тестів слід використовувати заглушки і фіктивні об'єкти?
- Чи є 100% покриття досяжною метою?
- Чи становить підвищена швидкість тесту з використанням БД в оперативній пам'яті додатковий ризик?
Я б порадив при виборі технологій для вашої команди керуватися тим, що корисно вам і вашим клієнтам. Не дозволяйте якомусь експерту переконати вас в тому, що, на вашу думку, невірно. Як завжди, через кілька років ми виявимо, що насправді у нас не було відповідей на всі питання. З'являться нові технології, а ті, які існують в даний час, виявляться в немилості. Те, що написано на папері, не завжди ідеально відображає практичне застосування Rails.
Ресурси для скачування
Схожі тими
- Оригінал статті: Real world Rails, Part 4: Testing strategies in Ruby on Rails (EN).
- ChangingThePresent.org - благодійний портал, який покладено в основу даної серії статей. Тут можна віддати в дар акр тропічного лісу, допомогти повернути зір сліпому або пожертвувати годину часу на дослідження раку (EN).
- завантажте ознайомчі версії продуктів IBM (EN).
- Дізнайтеся більше про безперервної інтеграції за допомогою CruiseControl.rb . Сервер безперервної інтеграції ThoughtWorks заощадив нам незліченну кількість годин (EN).
- Mock-об'єкти - це не заглушки : В цій статті Мартіна Фаулера (Martin Fowler) розповідається про різницю між заглушками і фіктивними об'єктами (EN).
- FlexMock - середовище фіктивних об'єктів Ruby, яка в даний час користується величезною популярністю (EN).
- Mocha - середовище фіктивних об'єктів, яка використовується в проекті ChangingThePresent (EN).
- RCov - відмінний інструмент аналізу покриття коду для Ruby (EN).
- Підпішіться на розсилку інформаційного бюлетеня developerWorks по Web-розробці (EN).
Підпішіть мене на ПОВІДОМЛЕННЯ до коментарів
Jsp?Nil?
Nil?
Nil?
Session [: user] = member?
Valid?
Чи є 100% покриття досяжною метою?
Чи становить підвищена швидкість тесту з використанням БД в оперативній пам'яті додатковий ризик?