Статьи

Как я тестирую

В недавней дискуссии в Google+ мой друг прокомментировал: « Разработка через тестирование (TDD) и разработка на основе поведения (BDD) — это Ivory Tower BS». Это побудило меня задуматься о моем первом проекте, о том, как я себя чувствовал. тогда, и как я к этому отношусь сейчас. С того первого проекта я разработал ритм TDD / BDD, который работает не только для меня, но и для клиента.

Ruby on Rails поставляется с набором тестов, который называется Test Unit , но многие разработчики предпочитают использовать RSpec, Cucumber или какую-то их комбинацию. Лично я предпочитаю последнее, используя комбинацию обоих.


С сайта 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 чтобы установить их.

Нам нужно запустить генераторы этих драгоценных камней, чтобы настроить их. Вы можете сделать это с помощью следующих команд терминала:

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, чтобы убедиться, что мои модели и их методы остаются под контролем. Я также часто использую его для тестирования контроллера высокого уровня, но это более детально, чем позволяет это руководство. Мы собираемся использовать ту же модель пользователя, которую мы ранее настроили с помощью нашей функции входа. Оглядываясь на результаты работы генератора моделей, мы видим, что мы также получили 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 намного перевешивают недостатки не раз.