Subscribed unsubscribe Subscribe Subscribe

テスト関連のGem(RSpec, FactoryGirl, Capybara, Poltergeist, Cucumber)

RSpec(testing framework)

基礎(rspec-core)

  • Example Group
describe "Using an array as a stack"
↓
class UsingAnArrayAsAStack < RSpec::Core::ExampleGroup

※before(:example) hookが実行されます

  • Example
属するExample Groupのコンテキストで評価されます
  • describe, context
ExampleGroupを作ります。

※ネストしたExampleGroupを作成する際にはdescribe or contextを使用します。
※describeをネストしても構いませんが、どういうコンテキストかを明示的にしたいときに使います。

  • it
describe or context内でitを使うと、Exampleを作れます。

※specify or exampleでも同様にExampleを作れます。
デバッグコードはit内に置いてください

  • shared_examples
Example Groupを他のExample Groupで共有することができます。

※include_examplesを使用

  • before
Exampleごとに実行されます

※引数に何も指定しない場合

  • subject テスト対象を共通化できます。
    マッチャーの対象は自動的にsubjectになります。
subject { hogehoge }
it { should be_empty } 
it { is_expected.to be_empty } #=> is_expected == expect(subject)
  • let 各Example内でキャッシュされます。
    遅延実行(呼び出された時点で実行)されます。
$count = 0
RSpec.describe "let" do
  let(:count) { $count += 1 }

  it "memoizes the value" do
    expect(count).to eq(1)
    expect(count).to eq(1)
  end

  it "is not cached across examples" do
    expect(count).to eq(2)
  end
end

基礎(rspec-its)

  • its 1つのExampleでの、ネストしたExample Groupを作ることができます。
    itsの引数はsubjectに対するものです。
its(:size)    { should eq(1) } #=> describe "" do it { subject.size should eq(1) } end
its("length") { should eq(1) }

基礎(rspec-expectations)

expect(actual).to eq(expected)  # passes if actual == expected
expect(actual).to match(/expression/)
expect(actual).to be_a(expected)              # passes if actual.kind_of?(expected)
expect(actual).to be_truthy   # passes if actual is truthy (not nil or false)
expect(actual).to be true     # passes if actual == true
expect { ... }.to raise_error(ErrorClass, "message")
expect { ... }.to throw_symbol(:symbol, 'value')
expect(actual).to be_xxx         # passes if actual.xxx?
expect(actual).to have_xxx(:arg) # passes if actual.has_xxx?(:arg)
expect([1, 2, 3]).to include(1)

基礎(rspec-rails)

  • Model Specs
it { should respond_to(:name) }
it { should be_valid }
it { should be_invalid }
it { should_not be_admin }
@user.save!
@user.toggle!(:admin)
it { should be_admin }
its(:remember_token) { should_not be_blank }
create(:micropost, user: @user, created_at: 1.day.ago)
create(:micropost, user: create(:user))
its(:followers) { should include(@user) }
its(:feed) { should include(newer_micropost) }
its(:followed_users) { should include(other_user) }
it { should be_following(other_user) }
  • Controller Specs
render_views
get :index
expect(response).to be_success
expect(response).to have_http_status(200)
expect(response).to render_template("index")
expect(assigns(:posts)).to match_array([post1, post2])
subject { post :create, campaign: attributes_for(:XXXXXXX) }
expect { subject }.to change(Xxxxxx, :count).by(1)
it { should redirect_to XXXXXX_path(XXXXXX) }

※ render_viewsはexpect(response.body).to match /piyo/imができるようになります
※viewスペックをgeneratorで作成しない場合はコントローラースペックでviewのテストも書けます ※mは改行も.でマッチさせるものです

  • Feature Specs

Feature specs test your application from the outside by simulating a browser. capybara is used to manage the simulated browser.

before { visit signup_path }
let(:submit) { "Create my account" }
it { should have_content('Sign up') }
it { should have_title(full_title('Sign up')) }
expect { click_button submit }.not_to change(User, :count)
before { click_button submit }
fill_in "Name",             with: "Example User"
expect { click_button submit }.to change(User, :count).by(1)
it { should have_link('Sign out') }
it { should have_selector('div.alert.alert-notice', text: 'User was successfully created.') }
it { should have_selector('div.pagination') }
expect(page).to have_selector('li', text: user.name)
let(:user) { create(:user) }
let(:micropost) { user.microposts }
let!(:m1) { create(:micropost, user: user, content: "Foo") }
let!(:m2) { create(:micropost, user: user, content: "Bar") }
expect(page).to have_selector('li', text: user.name)
before(:all) { 30.times { create(:user) } }
after(:all)  { User.delete_all }
expect do
  click_link('delete', match: :first)
end.to change(User, :count).by(-1)
it { should_not have_link('delete', href: user_path(admin)) }
patch(user_path(user), params)
specify { expect(user.reload).not_to be_admin }
  • Helper specs
expect(full_title("foo")).to match(/^Ruby on Rails Tutorial Sample App/)

FactoryGirl

factory_girl is a fixtures replacement with a straightforward definition syntax, support for multiple build strategies (saved instances, unsaved instances, attribute hashes, and stubbed objects), and support for multiple factories for the same class (user, admin_user, and so on), including factory inheritance.

https://github.com/thoughtbot/factory_girl/blob/master/GETTING_STARTED.md

  • モデル名 デフォルトではfactory名を"_"で分割したそれぞれの単語をキャメル化したものとモデル名は対応します。
    もし、factory名と異なる名前にしたい場合はclass:を使用します。
# This will guess the User class
FactoryGirl.define do
  factory :user do
    first_name "John"
    last_name  "Doe"
    admin false
  end

  # This will use the User class (Admin would have been guessed)
  factory :admin, class: User do
    first_name "Admin"
    last_name  "User"
    admin      true
  end
end

FactoryGirl.define do
  factory :user do
    sequence(:name)  { |n| "Person #{n}" }
    sequence(:email) { |n| "person_#{n}@example.com"}
    password "foobar"
    password_confirmation "foobar"

    factory :admin do
      admin true
    end
  end

  factory :micropost do
    content "Lorem ipsum"
    user
  end
end
  • rspecファイルからの使用方法
# Returns a saved User instance
user = create(:user)
# Returns a hash of attributes that can be used to build a User instance
attrs = attributes_for(:user)
  • springを使用している際の注意点 springはプリロードしますので、factory_girlへの変更を反映させたい場合はreloadの設定をする必要があります。

spec/spec_helper.rb

RSpec.configure do |config|
  ...
  # only reload once(not each top level example group)
  config.before(:suite) { FactoryGirl.reload }
  ...
end
  • 値の上書き
factory :user do
  first_name "Joe"
  last_name  "Blow"
  email { "#{first_name}.#{last_name}@example.com".downcase }
end

create(:user, last_name: "Doe").email
  • association
factory :user, aliases: [:author, :commenter] do
  first_name    "John"
  last_name     "Doe"
  date_of_birth { 18.years.ago }
end

factory :post do
  author
  # instead of
  # association :author, factory: :user
  title "How to read a book effectively"
  body  "There are five steps involved."
end

factory :comment do
  commenter
  # instead of
  # association :commenter, factory: :user
  body "Great article!"
end
FactoryGirl.define do

  # post factory with a `belongs_to` association for the user
  factory :post do
    title "Through the Looking Glass"
    user
  end

  # user factory without associated posts
  factory :user do
    name "John Doe"

    # user_with_posts will create post data after the user has been created
    factory :user_with_posts do
      # posts_count is declared as a transient attribute and available in
      # attributes on the factory, as well as the callback via the evaluator
      transient do
        posts_count 5
      end

      # the after(:create) yields two values; the user instance itself and the
      # evaluator, which stores all values from the factory, including transient
      # attributes; `create_list`'s second argument is the number of records
      # to create and we make sure the user is associated properly to the post
      after(:create) do |user, evaluator|
        create_list(:post, evaluator.posts_count, user: user)
      end
    end
  end
end
  • transient
factory :user do
  transient do
    rockstar true
    upcased  false
  end

  name  { "John Doe#{" - Rockstar" if rockstar}" }
  email { "#{name.downcase}@example.com" }

  after(:create) do |user, evaluator|
    user.name.upcase! if evaluator.upcased
  end
end

create(:user, upcased: true).name
#=> "JOHN DOE - ROCKSTAR"
  • trait
factory :user, aliases: [:author]

factory :story do
  title "My awesome story"
  author

  trait :published do
    published true
  end

  trait :unpublished do
    published false
  end

  trait :week_long_publishing do
    start_at { 1.week.ago }
    end_at   { Time.now }
  end

  trait :month_long_publishing do
    start_at { 1.month.ago }
    end_at   { Time.now }
  end

  factory :week_long_published_story,    traits: [:published, :week_long_publishing]
  factory :month_long_published_story,   traits: [:published, :month_long_publishing]
  factory :week_long_unpublished_story,  traits: [:unpublished, :week_long_publishing]
  factory :month_long_unpublished_story, traits: [:unpublished, :month_long_publishing]
end
factory :week_long_published_story_with_title, parent: :story do
  published
  week_long_publishing
  title { "Publishing that was started at #{start_at}" }
end
factory :user do
  name "Friendly User"

  trait :male do
    name   "John Doe"
    gender "Male"
  end

  trait :admin do
    admin true
  end
end

# creates an admin user with gender "Male" and name "Jon Snow"
create(:user, :admin, :male, name: "Jon Snow")

Capybara

Capybara helps you test web applications by simulating how a real user would interact with your app. It is agnostic about the driver running your tests and comes with Rack::Test and Selenium support built in. WebKit is supported through an external gem.

github.com

http://www.rubydoc.info/github/jnicklas/capybara

http://www.rubydoc.info/github/jnicklas/capybara/master

visit(post_comments_path(post))
expect(current_path).to eq(post_comments_path(post))

click_link('id-of-link')
click_link('Link Text')
click_button('Save')
click_on('Link Text') # clicks on either links or buttons
click_on('Button Value')

fill_in "Name", with: "Example User"
fill_in "Name", with: user.name #=> let(:user) { create(:user) }

choose('A Radio Button')
check('A Checkbox')
uncheck('A Checkbox')
attach_file('Image', '/path/to/image.jpg')
select('Option', from: 'Select Box')

expect(page).to have_selector('table tr')
expect(page).to have_selector(:xpath, '//table/tr')

expect(page).to have_xpath('//table/tr')
expect(page).to have_css('table tr.foo')
expect(page).to have_content('foo')

find_field('First Name').value
find_link('Hello', :visible => :all).visible?
find_button('Send').click

find(:xpath, "//table/tr").click
find("#overlay").find("h1").click
all('a').each { |a| a[:href] }

find('#navigation').click_link('Home')
expect(find('#navigation')).to have_button('Sign out')

within("li#employee") do
  fill_in 'Name', :with => 'Jimmy'
end

within(:xpath, "//li[@id='employee']") do
  fill_in 'Name', :with => 'Jimmy'
end

within_fieldset('Employee') do
  fill_in 'Name', :with => 'Jimmy'
end

within_table('Employee') do
  fill_in 'Name', :with => 'Jimmy'
end

facebook_window = window_opened_by do
  click_button 'Like'
end
within_window facebook_window do
  find('#login_email').set('a@example.com')
  find('#login_password').set('qwerty')
  click_button 'Submit'
end

page.execute_script("$('body').empty()")

result = page.evaluate_script('4 + 4');

accept_alert do
  click_link('Show Alert')
end

dismiss_confirm do
  click_link('Show Confirm')
end

accept_prompt(with: 'Linus Torvalds') do
  click_link('Show Prompt About Linux')
end

message = accept_prompt(with: 'Linus Torvalds') do
  click_link('Show Prompt About Linux')
end
expect(message).to eq('Who is the chief architect of Linux?')

# Debugging
save_and_open_page
print page.html
save_and_open_screenshot

click_link("Password") # also matches "Password confirmation"
Capybara.exact = true
click_link("Password") # does not match "Password confirmation"
click_link("Password", exact: false) # can be overridden

Capybara.run_server = false
Capybara.current_driver = :poltergeist
Capybara.app_host = 'http://www.google.com'
...
visit('/')

session = Capybara::Session.new(:webkit, my_rack_app)
session.within("//form[@id='session']") do
  session.fill_in 'Email', with: 'user@example.com'
  session.fill_in 'Password', with: 'password'
end
session.click_button 'Sign in'

Poltergeist

Poltergeist is another headless driver which integrates Capybara with PhantomJS. It is truly headless, so doesn't require Xvfb to run on your CI server. It will also detect and report any Javascript errors that happen within the page.

github.com

Cucumber

Cucumber lets software development teams describe how software should behave in plain text. The text is written in a business-readable domain-specific language and serves as documentation, automated tests and development-aid - all rolled into one format.

cucumber.io

github.com