Rails: Хватит отмазываться, начинаем BDD-ить!

Кто здесь?


Когда речь заходит о тестировании существующего продукта, а тем более о разработке чего-то нового на основе изначального написания сценариев использования, различных спецификаций и тестов, то частенько можно слышать подобные вещи:

11:24:21 PM Michael: ну хз, надо пробовать
11:24:24 PM Michael: наверное так лучше
11:24:27 PM Michael: даже я думаю наверняка
11:24:36 PM Michael: но пока меня че-то останавливает
11:24:38 PM Michael: лень наверное :)

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

Исходные данные


Для чистоты эксперимента, начнем все с нуля. Надеюсь, с RVM все давно уже дружат (если нет, бегом знакомиться). Отличная штука для управления версиями Ruby и всеми сопутствующими джемами. Ставится за одну команду в окошке терминала (ну или что там не в макосе, консоль, наверное). Сразу забываем о рутовских привилегиях.
Итак, создаем чистое окружение:

$ rvm gemset create bdd
$ rvm gemset use bdd

Все, никаких джемов, чистый космос. Ставим последние рельсы:

$ gem install rails --no-ri --no-rdoc
...
23 gems installed

Последние два ключа нужны, чтобы лишнее время не тратить и не ставить всякую ненужную документацию. Добавлял эти ключи постоянно, поэтому в конце концов отправил это дело в .gemrc:

$ echo 'gem: --no-ri --no-rdoc' >> ~/.gemrc

Отлично, рельсы есть. На момент написания статьи используем версию 3.0.3.

Собираем все джемы


Создаем чистое приложение:

$ rails new bdd

Сразу же открываем Gemfile, тот, что в корне. Если не открывается, то, видимо, не тот текстовый редактор, удаляем TextMate — ставим MacVim (все, закрыли тему, дальше молчу). Добавляем в файл все необходимое для тестирования. В нашем случае будем использовать Cucumber и RSpec, а также много всяких вкусностей, о которых расскажу чуть позже.

source 'http://rubygems.org'

gem 'rails', '3.0.3'
gem 'mysql2'

group :development, :test do
  gem 'rspec-rails'
  gem 'cucumber-rails'
end

group :test do
  gem 'capybara'
  gem 'database_cleaner'
  gem 'factory_girl_rails'
  gem 'email_spec'
  gem 'timecop'
  gem 'launchy'
end

Файл подправили, теперь все ставим:

$ bundle install
...
Your bundle is complete!..

Сразу же неплохо было бы в Gemfile зафиксировать все установленные версии, т.е. в явном виде прописать версию также, как у джема rails (тот, что самый первый), но в принципе для тестирования не критично, к тому же Gemfile.lock есть.
Дружим рельсы с RSpec:

$ rails g rspec:install

Дружим рельсы с Cucumber и попутно последнего с только что подруженным RSpec. Не забываем и про Capybara (да, и такое бывает, о нем чуть ниже):

$ rails g cucumber:install --rspec --capybara

Наведем красивости:

$ echo '--colour --format documentation' > .rspec

После этого RSpec будет выводить информацию по прохождению тестов в красивом формате. То же самое можно сделать и для Cucumber. Открываем config/cucumber.yml и выполняем :%s/progress/pretty/g (заменяем слово progress на pretty).
Наконец, заигнорим папку test (на всякий случай, вдруг чего) и прочий хлам:

$ echo 'test' >> .gitignore
$ echo '*.swp' >> .gitignore
$ echo '.DS_Store' >> .gitignore

Все, что понаделали, занесем в git (тут тоже должно быть без вопросов, дядю Линуса все знают):

$ git init .
$ git add .
$ git commit -m 'Bare Rails 3.0.3 application with BDD environment'

Так, вот тут уже капучино с корицей в сторону, приступаем непосредственно к разработке на основе подхода BDD. Тут тоже надеюсь на наличие общего представления об этом злом монстре (спокойно, добрый он). Гугл в помощь.

Кто есть кто?


Cucumber

https://github.com/aslakhellesoy/cucumber
Итак, Cucumber, он же огурец, — это инструмент для «элегантного и радостного BDD», о чем можно прочесть на его главной странице. Да, радости будет хоть отбавляй, особенно в первое время с непривычки. Про RSpec, думаю, авторы могли бы сказать нечто подобное. В любом случае по этим друзьям документации и примеров очень много, чего только стоят одни wiki-странички на гитхабе, поэтому особо распространяться не буду, только коротенько. Сразу оговорюсь, что все в контексте рельсов, хотя, конечно, этим ни RSpec, ни Cucumber не ограничиваются.
Вернемся к Cucumber. Он позволяет описать поведение системы с позиции внешнего наблюдателя (читать как «заказчика, конечного пользователя»). При этом описание дается на естественно языке, никаких вам begin end. Каждый вариант использования системы в огурце называется «фичей» (feature). Все они лежат в одноименной папочке features с непредсказуемым расширением файла *.feature. В каждом файле описывается один или несколько «сценариев» (scenario), характеризующих фичу. Сценарии состоят из ряда шагов, объявленных в файлах из папки features/step_definitions/*_steps.rb. Шаги бывают трех типов: Given (что-то данное, некоторое предварительное условие), When (что-то, что происходит, какие-то действия пользователя) и Then (результат, реакция, отклик), но о таких вещах лучше на живых примерах говорить. Одна из последних моих фич (в НГ по телевизору слышал «я не волшебник, я только учус…», в общем здесь очень даже подходит, не судите строго):

@javascript @redis
Feature: A user sees who is online
  In order to know that the portal is alive
  As a regular user
  I want to see who is online

  Background:
    Given the section "Personal profiles" exists

  Scenario Outline: See a special label next to the name of a user who is online
    Given nobody has been on the portal for a lot time
    And a user exists with name: "<name>"
    When the user "<name>" <user action> the portal
    And I go to the profile of the user "<name>"
    Then I should <my action> the online label for the user "<name>"
    When I go to the section "Personal profiles"
    Then I should <my action> the online label for the user "<name>"

    Examples:
      | name     | user action    | my action |
      | Victoria | visits         | see       |
      | Michael  | does not visit | not see   |

Ключевое слово "Feature" предшествует имени фичи, по правилам должно кратко и ясно описывать действия пользователя. Три строчки под ним говорят, для чего, кто и что хочет. Раздел "Background" по желанию, задает общие шаги для всех сценариев фичи. Далее идут либо "Scenario", либо "Scenario Outline". Первое описывает сценарий без параметров, второе — с параметрами, идущими после ключевого слова "Examples" снизу. Через собачку (@) расставляются метки, позволяющие наложить дополнительные условия на всю фичу или конкретный сценарий, а так же выборочно выполнить тест. В принципе все читабельно. В идеале должно быть так, чтобы заказчик взял этот файл, открыл простым текстовым редактором и не напрягаясь прочел, все понял, осознал и подтвердил: «Оно!»
Что касается настройки огурца, то она проводится в сгенерированном файле features/support/env.rb. Там добавляются необходимые строчки кода для подключения всего, что необходимо для тестирования.

RSpec

https://github.com/rspec/rspec
RSpec тоже используется для описания внешнего поведения системы, однако больше подходит для копания в ее внутренностях. Т. е. если нужно проверить, что какая-то модель ведет себя адекватно или что какая-нибудь самописная библиотека оправдывает возложенные на нее надежды, то RSpec самое то. Но если же хочется проверить, что «пользователь, введя в первое окошко свой e-mail, а во второе — пароль, увидел сообщение об успешном входе в систему», то это к Cucumber. Каждый файл у RSpec в простонародье называется «спекой» (spec, от specification), находится в папке spec и заканчивается на *_spec.rb. Принято файлы в папке spec разбивать по вложенным папкам, чтобы они отражали структуру рельсового проекта (models, controllers, helpers, etc.). Вот слегка наигранный пример:

describe User do
  it { should ensure_length_of(:email).is_at_least(6).is_at_most(100) }
  it { should validate_format_of(:email).with('ma1f0rmed emai1 address') }

  subject { Factory :user }
  it { should validate_uniqueness_of :email }
  it { should validate_uniqueness_of :address }
end

Тут, чувствую, без Shoulda не обошлось, но речь не об этом, а о том, что так приблизительно выглядят спеки.
Конфигурация RSpec сконцентрирована в файле spec/spec_helper.rb. Аналогично env.rb у Cucumber сюда идут все вспомогательные require и прочее.

Capybara

https://github.com/jnicklas/capybara
Capybara — это удобная штука для автоматизации «браузерного» тестирования приложения. Судя по иллюстрациям из гугла это какая-то морская свинка, но речь не о ней. Джем дает множество вспомогательных методов в тестовое окружение, в результате чего с легкостью можно проверить такие вещи как, например, переход по ссылкам, заполнение форм (поля ввода, выпадающие списки, чекбоксы и так далее), наличие какого-либо элемента на странице. Более того, при подключении к нему другой штуки с не менее загадочным названием Selenium, позволяет проходить шаги тестового сценария прямо в браузере, в прямом смысле. У вас откроется, например, любимый Chrome и странички будут бегать одна за другой. В результате, можно отлаживать и JavaScript. Интересно было наблюдать в первый раз как оно само собой проверяло сортировку списка на страничке путем перетаскиивания его элементов. Selenium не единственный движок, с которым умеет работать Capybara, но является, на мой взгляд, самым удобным, да и не требует установки ничего лишнего, типа JRuby. Selenium достаточно легко попросить проверить что-либо не только в Chrome, но и в Firefox (вариант по умолчанию) и даже в осле. В случае Cucumber для этого достаточно открыть файл env.rb и добавить следующий код с указанием нужного браузера (список доступных смотрим в документации по Selenium):

Capybara.register_driver :selenium do |app|
  Capybara::Driver::Selenium.new app, :browser => :chrome
end


Database Cleaner

https://github.com/bmabey/database_cleaner
Джем для чистки базы данных перед или после тестов. Удобно, чтобы замести следы и начать с нуля. Поддерживает разные БД и адаптеры к ним. Использую его как для чистки MySQL + ActiveRecord, так и mongoDB + Mongoid.
Если этот джем подключен в Gemfile, то при генерации окружения Cucumber добавит его в свой env.rb автоматически, что-то вроде:

require 'database_cleaner'
DatabaseCleaner.strategy = :truncation
Before do
  DatabaseCleaner.clean
end

Для RSpec можно прописать в spec_helper.rb следующее (разберетесь куда):

config.before(:suite) do
  DatabaseCleaner.strategy = :truncation
end

config.before(:each) do
  DatabaseCleaner.clean
end


Factory Girl Rails

https://github.com/thoughtbot/factory_girl_rails
О, и до девочек дошли! Так, эту библиотеку используют для удобного создания экземпляров моделей (замена нативных для рельсов fixtures, если кому-то это о чем-то скажет). Другими словами, нужно же на примере чего-то тестировать. Нам нужны и пользователи (User) какие-нибудь, и статьи (Article), и проекты (Project) с задачами (Task). Не создавать же каждый раз объекты с заполнением всех обязательных полей. Вот тут-то на помощь и приходят подобные фабрики. Достаточно один раз определить шаблон и далее генерировать новые сущности на его основе. В связке с RSpec фабрики обычно хранятся в папке spec/factories или напрямую в spec/factories.rb. Например, вот такая:

Factory.define :user do |factory|
  factory.sequence(:email) { |i| "user#{ i }@example.org" }
  factory.password 'password'
  factory.password_confirmation { |user| user.password }
  factory.confirmed_at Time.now
end

Все, теперь в тестах достаточно Factory(:user) и у нас тепленький валидный юзер.

Email Spec

https://github.com/bmabey/email-spec
По названию наверное так сразу и не скажешь, но джем используется для тестирования отправки почты. Конечно, нативно вписывается как в Cucumber, так и в RSpec. В env.rb пишем:

require 'email_spec' # add this line if you use spork
require 'email_spec/cucumber'

В spec_helper.rb:

require "email_spec"

Далее:

$ rails g email_spec:steps

Добавит вспомогательные готовые шаги для тестов.

Timecop

https://github.com/jtrupiano/timecop
Вот эта штука мне особенно понравилась, позволяет путешествовать во времени. Сначала, что-то самописное использовал, а потом наткнулся на этого копа. Незаменима, когда нужно проверить что-то завязанное на времени, например, «поставив в печку пирожок, подождав четыре часа, мы получаем угольки». Просмотрите обязательно статью о нем (в списке литературы), там и шаги есть для Cucumber. Единственное, что хотелось бы заметить, тесты со временем лучше помечать особой меткой (да-да, те самые метки, как в Cucumber, так и в RSpec), например, @travel_through_time для того, чтобы после них всегда возвращаться в настоящее время, а то инструменты будут с ума сходить. У меня в env.rb есть следующие строки:

After('@travel_through_time') do
  Timecop.return
end

Строчки говорят огурцу исполнить код после каждого сценария с соответствующей меткой. В RSpec можно сделать нечто подобное.

Launchy

https://github.com/copiousfreetime/launchy
Иногда бывает так, что тест валится (бывает же), и сложно определить почему. Хочется взглянуть на страницу, на которой, например, Cucumber не нашел кнопку «Создать». Для этих целей ставится этот джем и в шагах огурца добавляется нечно вроде:

Then 'WTF?' do
  save_and_open_page
end

Дальше просто в сценарии вызываем соответствующий шаг перед тем, который не проходит, и видим в своем браузере по умолчанию сохраненную страничку с формой входа на сайт и надписью «У вас недостаточно прав для выполнения данной операции», что-то в таком духе.

В заглючение :%s/глюч/ключ/g


Окружение настроено и готово к издевательству над собой в полной мере. Конечно, после этого гарантий никаких нет, что оно так и не останется просто настроенным, а будет активно использоваться. Все в наших руках, вообще все, не только написание буковок кода, вся жизнь, ммм да. Но, возвращаясь к BDD (надеюсь хотя бы кто-нибудь посмотрел к заключению расшифровку), знаю по себе, что пока не заставишь себя описывать поведение системы заранее (да и после уже тоже неплохо), не поймешь до конца, как это важно, а главное удобно и полезно. Сейчас мне даже бывает не нужно открывать браузер, чтобы убедиться, что все работает, а особенно, когда рельсы зажирают все мозги бедной макоси. После такого процесса разработки всегда появляются фичи и спеки, тестирующие какую-то часть функциональности, которые в последствии легко запустить:

$ rspec spec
$ cucumber

И вот тут-то уже станет видно, что ты или твой напарник… молодец, одним словом.
Спасибо за внимание, напишу еще, если тематика затронула тонкие струны чьей-нибудь трепетной души. Все, пора спать.

Список литературы (как в школе)


Про Cucumber:

Про RSpec:

Про Райана Бейтса (Ryan Bates):

Про Timecop:

Про RVM:

Про Git:

// vim: set ft=habrahabr
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 38

    0
    все указанное работает с rails 3?
      0
      Да, на вторых давно ничего не пишу.
      +5
      Вместо девочек лучше юзать fabrication — там есть генераторы, да и интерфейсы красивее. И не вижу Faker/Forgery — куда без них.
        0
        Ммм, fabrication и Forgery интересно, до этого пробовали машиниста, но откатились к девочкам, уж больно он тупой (мужлан:)
          0
          Ух ты, и правда, fabrication прикольный, спасибо за наводку!
            0
            Плюс про Faker как то забыли
              0
              Ага, и ещё Faker, конечно же. )
              0
              Почитал только что про fabrication. Сколь-нибудь заметных отличий от последних девочек не нашел. :-)
              +1
              Еще autotest вместе с autotest-growl забыли, хотя это, конечно, по вкусу
                0
                Ребята, ну не все же сразу вываливать) Делитесь своими рецептами, очень интересно!
                  0
                  Ребята, ну не все же сразу) Делитесь своими рецептами, очень интересно!
                  0
                  то, что вы разобрались в таком количестве инструментов — заслуживает уважения. Думаю, можно было бы еще упомянуть faker (http://faker.rubyforge.org/rdoc/).
                    +1
                    Только FFaker, на родителя забили уже давно.
                      0
                      thx
                    +1
                    Отличный материал, спасибо!
                      +5
                      Пост в котором рельсовики делятся bdd-гемами [x]
                        0
                        Спасибо за статью. Обязательно прочитаю от начала и до конца!
                        (Немного самовнушения в этом деле не помешает :D)
                          +2
                          Скажите, кто-то большой боевой проект реализовывал по всем правилам?
                          А то, почему-то обычно требуется делать Code Fast, а не Code Well.
                            +2
                            Автоматизированное тестирование — это код фаст как раз. Просто научиться надо.
                              0
                              Интересно, сколько времени в процентах уходит на написание тестов при оптимальном покрытии, если научиться и делать быстро. Там ведь много кейсов)
                                +1
                                Не засекал. Но могу сказать точно — однозначно быстрее, чем тестирование руками.
                              +1
                              У нас большой боевой проект с покрытием около 80%, Code Fast без тестов заканчивается на первом же рефакторинге. Вообще нужно брать за правило, что чем больше взаимосвязей между сущностями приложения — тем больше должно быть покрытие (в данном проекте ERD-диаграмма похожа на карту метро Лондона)
                              +1
                              И действительно, всё так просто.
                                +2
                                В закладки!
                                  0
                                  Спасибо за статью, сразу в избранное кинул.

                                  Не хватает только инфы как тестировать что-то при условии что юзер залогинился в систему

                                  Правда я в то время пользовался restful_authentication — и не смог прикрутить его к тестам, а сейчас перешел на authlogic.
                                    0
                                    Для того же Devise или Authlogic есть вспомогательные хелперы, которые можно отыскать на гитхабе. В общем случае можно просто зайти на страничку логина и ввести все данные, например, я использую следующий шаг в features/step_definitions/user_steps.rb:

                                    Given /^I am signed in$/ do
                                      user = Factory(:user)
                                      visit new_user_session_path
                                      fill_in 'user[email]', :with => user.email
                                      fill_in 'user[password]', :with => user.password
                                      click_button I18n.t('formtastic.actions.enter')
                                      Then %{I should be on the home page}
                                      Then %{I should see "#{ I18n.t 'devise.sessions.user.signed_in' }"}
                                    end
                                    
                                    +3
                                    Про самое приятное забыл написать, всегда отрадно видеть нечто подобное:

                                    $ rspec spec
                                    ...
                                    Finished in 20.92 seconds
                                    45 examples, 0 failures
                                    
                                    $ cucumber
                                    ...
                                    102 scenarios (102 passed)
                                    986 steps (986 passed)
                                    3m24.668s
                                    
                                      0
                                      «Иногда бывает так, что тест валится (бывает же)». Вообще философия BDD как раз и заключается в том, чтобы тест сначала валился, а уже потом все работало. То есть сначала описываем систему, ничего не работает, тесты все фейл и уже потом начинаем добавлять нужный функционал. Надо так же отметить, что нужно добавлять именно столько функционала, сколько нужно чтобы прошел тест. Улучшение кода, добавление, допиливание идет уже на фазе рефакторинга.
                                      То есть получается триада:
                                      — Тест фейл
                                      — Тест пасс
                                      — Рефакторинг
                                        0
                                        Да, совершенно верно, на этом BDD и основывается. Разработку я начинаю с написания фичи, которая, естественно, не проходит.

                                        В данном случае подразумевается, что вроде и написал код, который должен удовлетворять какому-либо шагу фичи, но все равно что-то не так, и хочется посмотреть, что именно. Просто в начале я постоянно натыкался на что-то вроде «permission denied…» из-за того, что фича тестируется от «чистого» юзера, у которого нет ничего лишнего. В результате, если постоянно не проверяешь на какой странице находишься, то не понимаешь, почему же Capybara не видит поля для ввода, например.

                                        Кстати, упрощенная версия шага "WTF?</>":

                                        Then 'WTF?' do
                                          puts page.body
                                        end
                                        
                                        +2
                                          0
                                          Согласен, отлично иллюстрирует процесс разработки на оснвое BDD, да и вся книжка, на мой взгляд, неплохая, всем советую почитать.
                                        0
                                        Спасибо за статью!
                                        Правда у меня проблемка возникла, cucumber падает с сообщением:

                                        superclass mismatch for class SQLite3Adapter

                                        Хотя использую mysql и database_cleaner 0.6.0

                                        Не сталкивались?
                                          0
                                          Накоецто обширная статья о разработке через тестирование, а то все говорят что это такое и как круто но никто не говорит как с этим делом работать, а до The RSpec Book руки пока не дошли.
                                          Спасибо Вам и комментирующим за подборку гемов, с ними все понятно они все специализированны (Email, Mock'и, проверка структуры DOM и тд), но мне как еще только начинающему BDD'ить не очень понятна с точки зрения разработчика отличие между огруцом и RSpec. Я понимаю что на огурец поидеи смотрит заказчик и он написан на нативном языке а RSpec более программный, но поидеи то тестировать они могут одно и тоже? или должны одно и тоже? и если разное то что именно?
                                            0
                                            Да, тоже долго смущало. Но для себя я определил, что все дело в языке, в том, как описывается поведение системы. Конечно, что можно проверить на Cucumber, то же самое можно проверить и на RSpec, и наоборот. На самом деле каждый шаг огурца в конечном счете описывается на том же Ruby should'ами RSpec (в данном окружении).

                                            В статье написал, что RSpec подходит больше для внутренностей, чего-то более приближенного к коду, что собственно рисунок выше и иллюстрирует. Слабо себе представляю фичу огурца «Функция foo должна возвращать bar при передаче ей baz», а вот сценарий поведения пользователя совсем другое дело.
                                              0
                                              Cucumber — для приемочного тестирования, RSpec – для модульного.
                                              0
                                              у меня story на русском были написаны, сейчас пример вытащу

                                              Функционал: Админ может добавлять, удалять и изменять статьи
                                              Что бы админ мог управлять статьями
                                              Как администратор сайта
                                              Я должен иметь возможность удалять, добавлять и изменять статьи
                                              Как пользователь
                                              Я должен видеть отредактированные статьи

                                              Предыстория:
                                              Допустим админ существует
                                              И админ авторизован

                                              Сценарий: добавление новой статьи
                                              И видит список статей в админзоне
                                              Если создать новую статью с заголовком "bbb" url "my-new-article" и телом "bla-bla-bla"
                                              То увидим в списке статей заголовок "bbb"

                                              Сценарий: редактирование существующей статьи
                                              И существует статья с заголовком "bbb" url "my-new-article" и телом "bla-bla-bla"
                                              Если admin редактирует статью "bbb" и сохраняет тело "no-blabla"
                                              То пользователь должен увидить тело статьи "no-blabla" по адресу "/articles/my-new-article"

                                              Сценарий: удалить существующую статью foo
                                              И существует статья foo с url foo
                                              И статья с url foo опубликована
                                              Если админ удаляет статью с урл foo
                                              То юзер в списке статей не видит статью "foo"


                                              When /^admin edit article (.*) and save content (.*)$/ do |url, content|
                                              visit edit_admin_article_url(Article.find_by_url(url))
                                              fill_in 'article[body]', :with=>content
                                              click_button "Сохранить статью"
                                              response.should be_success
                                              end

                                              Given /^статья с url (.*) опубликована$/ do |url|
                                              @article = Article.find_by_url(url)
                                              @article.published.should be_true
                                              visit "/articles/#{url}"
                                              response.should be_success
                                              end

                                              When /^админ удаляет статью с урл (.*)$/ do |url|
                                              delete admin_article_url(Article.find_by_url(url))
                                              end

                                              Then /^юзер в списке статей не видит статью "(.*)"$/ do |article|
                                              visit "/articles"
                                              assert_not_contain article
                                              end

                                              Given /^посетитель открывает страницу "([^\"]*)"$/ do |page|
                                              visit page
                                              end

                                              When /^посетитель нажмет на ссылку "([^\"]*)"$/ do |link|
                                              click_link link
                                              end

                                              Then /^посетитель нажмет на ссылку (.*)$/ do |link|
                                              assert_contain link
                                              click_link link
                                              end
                                              Then /^перейдет на страницу статьи (.*)$/ do |title|
                                              @article = Article.find_by_title(title)
                                              visit article_url(@article.url)
                                              end
                                              Given /^посетитель открывает страницу статей$/ do
                                              visit articles_path
                                              end
                                              Then /^увидит полный текст "(.*)" статьи с названием (.*)$/ do |body, title|
                                              assert_contain(body)
                                              assert_contain(title)
                                              end

                                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                              Самое читаемое