В недавней дискуссии в Google+ мой друг прокомментировал: « Разработка через тестирование (TDD) и разработка на основе поведения (BDD) — это Ivory Tower BS». Это побудило меня задуматься о моем первом проекте, о том, как я себя чувствовал. тогда, и как я к этому отношусь сейчас. С того первого проекта я разработал ритм TDD / BDD, который работает не только для меня, но и для клиента.
Ruby on Rails поставляется с набором тестов, который называется Test Unit , но многие разработчики предпочитают использовать RSpec, Cucumber или какую-то их комбинацию. Лично я предпочитаю последнее, используя комбинацию обоих.
RSpec
С сайта RSpec :
RSpec — это инструмент тестирования для языка программирования Ruby. Созданная под лозунгом Behavior-Driven Development, она разработана для того, чтобы сделать Test-Driven Development продуктивным и приятным опытом.
RSpec предоставляет мощный DSL, который полезен как для модульного, так и для интеграционного тестирования. Хотя я использовал RSpec для написания интеграционных тестов, я предпочитаю использовать его только в качестве модульного тестирования. Поэтому я расскажу, как я использую RSpec исключительно для модульного тестирования. Я рекомендую прочитать Книгу RSpec Дэвида Челимского и других для полного и всестороннего освещения RSpec.
Огурец
Я обнаружил, что преимущества TDD / BDD намного перевешивают недостатки.
Cucumber — это среда интеграции и приемочного тестирования, которая поддерживает Ruby, Java, .NET, Flex и множество других веб-языков и сред. Его истинная сила исходит от DSL; он не только доступен на простом английском языке, но и переведен более чем на сорок разговорных языков.
С помощью удобочитаемого приемочного теста вы можете заставить клиента подписать какую-либо функцию, прежде чем писать одну строку кода. Как и в случае с RSpec, я буду освещать огурец только в той емкости, в которой я его использую. Для полного изложения на Огуреке, проверьте Книгу Огурец .
Настройка
Давайте сначала начнем новый проект, инструктируя Rails пропустить Test Unit. Введите в терминал следующее:
1
|
rails new how_i_test -T
|
В Gemfile
добавьте:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
source ‘https://rubygems.org’
…
group :test do
gem ‘capybara’
gem ‘cucumber-rails’, require: false
gem ‘database cleaner’
gem ‘factory_girl_rails’
gem ‘shoulda’
end
group :development, :test do
gem ‘rspec-rails’
end
|
В основном я использую RSpec, чтобы убедиться, что мои модели и их методы остаются под контролем.
Здесь мы поместили Cucumber и его друзей в блок группового test
. Это гарантирует, что они правильно загружены только в тестовой среде Rails. Обратите внимание, как мы также загружаем RSpec внутри блоков development
и test
, делая его доступным в обеих средах. Есть несколько других драгоценных камней. который я кратко опишу ниже. Не забудьте запустить bundle install
чтобы установить их.
- Капибара : имитирует взаимодействие с браузером.
- Очиститель базы данных : очищает базу данных между тестовыми запусками.
- Factory Girl Rails : замена крепежа.
- Следует : вспомогательные методы и средства сопоставления для RSpec.
Нам нужно запустить генераторы этих драгоценных камней, чтобы настроить их. Вы можете сделать это с помощью следующих команд терминала:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
rails g rspec:install
create .rspec
create spec
create spec/spec_helper.rb
rails g cucumber:install
create config/cucumber.yml
create script/cucumber
chmod script/cucumber
create features/step_definitions
create features/support
create features/support/env.rb
exist lib/tasks
create lib/tasks/cucumber.rake
gsub config/database.yml
gsub config/database.yml
force config/database.yml
|
На этом этапе мы могли бы начать писать спецификации и тесты для тестирования нашего приложения, но мы можем настроить несколько вещей, чтобы упростить тестирование. Начнем с файла application.rb
.
01
02
03
04
05
06
07
08
09
10
11
|
module HowITest
class Application < Rails::Application
config.generators do |g|
g.view_specs false
g.helper_specs false
g.test_framework :rspec, :fixture => true
g.fixture_replacement :factory_girl, :dir => ‘spec/factories’
end
…
end
end
|
Внутри класса Application мы переопределяем несколько генераторов Rails по умолчанию. Для первых двух мы пропускаем спецификации поколений представлений и помощников.
Эти тесты не нужны, потому что мы используем RSpec только для модульных тестов.
Третья строка сообщает Rails, что мы намерены использовать RSpec в качестве нашей тестовой среды, и она также должна генерировать фиксаторы при генерации моделей. Последняя строка гарантирует, что мы используем factory_girl для наших приборов, которые созданы в каталоге spec/factories
.
Наша первая особенность
Для простоты мы напишем простую функцию для входа в наше приложение. Для краткости я пропущу реальную реализацию и остановлюсь на комплекте тестирования. Вот содержимое features/signing_in.feature
:
01
02
03
04
05
06
07
08
09
10
11
|
Feature: Signing In
In order to use the application
As a registered user
I want to sign in through a form
Scenario: Signing in through the form
Given there is a registered user with email «[email protected]»
And I am on the sign in page
When I enter correct credentials
And I press the sign in button
Then the flash message should be «Signed in successfully.»
|
Когда мы запускаем это в терминале с помощью cucumber features/signing_in.feature
, мы видим много выходных данных, заканчивающихся нашими неопределенными шагами:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
Given /^there is a registered user with email «(.*?)»$/ do |arg1|
pending # express the regexp above with the code you wish you had
end
Given /^I am on the sign in page$/ do
pending # express the regexp above with the code you wish you had
end
When /^I enter correct credentials$/ do
pending # express the regexp above with the code you wish you had
end
When /^I press the sign in button$/ do
pending # express the regexp above with the code you wish you had
end
Then /^the flash message should be «(.*?)»$/ do |arg1|
pending # express the regexp above with the code you wish you had
end
|
Следующим шагом является определение того, что мы ожидаем от каждого из этих шагов. Мы выражаем это в features/stepdefinitions/signin_steps.rb
, используя обычный Ruby с селекторами features/stepdefinitions/signin_steps.rb
и CSS.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
Given /^there is a registered user with email «(.*?)»$/ do |email|
@user = FactoryGirl.create(:user, email: email)
end
Given /^I am on the sign in page$/ do
visit sign_in_path
end
When /^I enter correct credentials$/ do
fillin «Email», with: @user.email
fillin «Password», with: @user.password
end
When /^I press the sign in button$/ do
click_button «Sign in»
end
Then /^the flash message should be «(.*?)»$/ do |text|
within(«.flash») do
page.should have_content text
end
end
|
В каждом из блоков Given
, When
и Then
мы используем DSL Capybara, чтобы определить, что мы ожидаем от каждого блока (кроме первого). В первом данном блоке мы говорим factory_girl создать пользователя, сохраненного в переменной экземпляра user
для последующего использования. Если вы снова запустите cucumber features/signing_in.feature
, вы должны увидеть что-то похожее на следующее:
1
2
3
4
5
|
Scenario: Signing in through the form # features/signing_in.feature:6
Given there is a registered user with email «[email protected]» # features/step_definitions/signing\_in\_steps.rb:1
Factory not registered: user (ArgumentError)
./features/step_definitions/signing\_in\_steps.rb:2:in `/^there is a registered user with email «(.*?)»$/’
features/signing_in.feature:7:in `Given there is a registered user with email «[email protected]»‘
|
Из сообщения об ошибке видно, что в нашем примере происходит сбой в строке 1 с ArgumentError
пользовательской фабрики, которая не зарегистрирована. Мы могли бы создать эту фабрику сами, но некоторая магия, которую мы настроили ранее, заставит Rails сделать это для нас. Когда мы генерируем нашу пользовательскую модель, мы получаем пользовательскую фабрику бесплатно.
1
2
3
4
5
6
7
8
|
rails g model user email:string password:string
invoke active_record
create db/migrate/20121218044026\_create\_users.rb
create app/models/user.rb
invoke rspec
create spec/models/user_spec.rb
invoke factory_girl
create spec/factories/users.rb
|
Как видите, генератор моделей вызывает factory_girl и создает следующий файл:
1
2
3
4
5
6
7
|
ruby spec/factories/users.rb
FactoryGirl.define do
factory :user do
email «MyString»
password «MyString»
end
end
|
Я не буду вдаваться в подробности factory_girl здесь, но вы можете прочитать больше в их руководстве по началу работы. Не забудьте запустить rake db:migrate
и rake db:test:prepare
для загрузки новой схемы. Это должно пройти первый шаг нашей функции, и вы начнете свой путь использования Cucumber для тестирования интеграции. При каждом прохождении ваших функций, Cucumber будет направлять вас к тем частям, которые он считает недостающими, чтобы он прошел.
Тестирование модели с RSpec и musta
В основном я использую RSpec, чтобы убедиться, что мои модели и их методы остаются под контролем. Я также часто использую его для тестирования контроллера высокого уровня, но это более детально, чем позволяет это руководство. Мы собираемся использовать ту же модель пользователя, которую мы ранее настроили с помощью нашей функции входа. Оглядываясь на результаты работы генератора моделей, мы видим, что мы также получили user_spec.rb
бесплатно. Если мы запустим rspec spec/models/user_spec.rb
мы должны увидеть следующий вывод.
1
2
|
Pending:
User add some examples to (or delete) /Users/janders/workspace/how\_i\_test/spec/models/user_spec.rb
|
И если мы откроем этот файл, мы увидим:
1
2
3
4
5
|
require ‘spechelper’
describe User do
pending «add some examples to (or delete) #{FILE}»
end
|
Ожидающая линия дает нам вывод, который мы видели в терминале. Мы будем использовать средства сравнения ActiveRecord и ActiveModel, чтобы наша пользовательская модель соответствовала нашей бизнес-логике.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
require ‘spechelper’
describe User do
context «#fields» do
it { should respondto(:email) }
it { should respondto(:password) }
it { should respondto(:firstname) }
it { should respondto(:lastname) }
end
context «#validations» do
it { should validate_presence_of(:email) }
it { should validate_presence_of(:password) }
it { should validate_uniqueness_of(:email) }
end
context «#associations» do
it { should have_many(:tasks) }
end
describe «#methods» do
let!(:user) { FactoryGirl.create(:user) }
it «name should return the users name» do
user.name.should eql «Testy McTesterson»
end
end
end
|
Мы настроили несколько контекстных блоков внутри нашего первого блока описаний, чтобы проверить такие вещи, как поля, проверки и ассоциации. Хотя между describe
и блоком context
нет функциональных различий, существует контекстуальный. Мы используем блоки describe
для установки состояния тестируемого объекта и context
блоки для группировки этих тестов. Это делает наши тесты более удобочитаемыми и поддерживаемыми в долгосрочной перспективе.
Первое
describe
позволяет нам протестировать модельUser
в неизмененном состоянии.
Мы используем это неизмененное состояние для проверки базы данных, в которой сопоставления Должа группируются по типу. Следующий блок описаний настраивает пользователя из нашей ранее созданной фабрики user
. Настройка пользователя с помощью метода let
внутри этого блока позволяет нам проверить экземпляр нашей пользовательской модели на соответствие известным атрибутам.
Теперь, когда мы запускаем rspec spec/models/user_spec.rb
, мы видим, что все наши новые тесты rspec spec/models/user_spec.rb
сбой.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
Failures:
1) User#methods name should return the users name
Failure/Error: user.name.should eql «Testy McTesterson»
NoMethodError:
undefined method name’ for #<User:0x007ff1d2775170>
# ./spec/models/user_spec.rb:26:in</code>block (3 levels) in <top (required)>’
2) User#validations
Failure/Error: it { should validate_uniqueness_of(:email) }
Expected errors to include «has already been taken» when email is set to «arbitrary<em>string», got no errors
# ./spec/models/user</em>spec.rb:15:in `block (3 levels) in <top (required)>’
3) User#validations
Failure/Error: it { should validate_presence_of(:password) }
Expected errors to include «can’t be blank» when password is set to nil, got no errors
# ./spec/models/user_spec.rb:14:in `block (3 levels) in <top (required)>’
4) User#validations
Failure/Error: it { should validate_presence_of(:email) }
Expected errors to include «can’t be blank» when email is set to nil, got no errors
# ./spec/models/user_spec.rb:13:in `block (3 levels) in <top (required)>’
5) User#associations
Failure/Error: it { should have<em>many(:tasks) }
Expected User to have a has</em>many association called tasks (no association called tasks)
# ./spec/models/user_spec.rb:19:in `block (3 levels) in <top (required)>’
6) User#fields
Failure/Error: it { should respond<em>to(:last</em>name) }
expected #<User id: nil, email: nil, password: nil, created_at: nil, updated_at: nil> to respond to :last<em>name
# ./spec/models/user</em>spec.rb:9:in `block (3 levels) in <top (required)>’
7) User#fields
Failure/Error: it { should respond<em>to(:first</em>name) }
expected #<User id: nil, email: nil, password: nil, created_at: nil, updated_at: nil> to respond to :first<em>name
# ./spec/models/user</em>spec.rb:8:in <code>block (3 levels) in <top (required)>’
|
В случае неудачи каждого из этих тестов у нас есть структура, необходимая для добавления миграций, методов, ассоциаций и проверок в наши модели. По мере развития нашего приложения, расширения моделей и изменения нашей схемы этот уровень тестирования обеспечивает нам защиту от внесения критических изменений.
Вывод
Несмотря на то, что мы не затронули слишком много тем, у вас теперь должно быть базовое понимание интеграции и модульного тестирования с Cucumber и RSpec. TDD / BDD — это одна из тех вещей, которые разработчики либо делают, либо не делают, но я обнаружил, что преимущества TDD / BDD намного перевешивают недостатки не раз.