読者です 読者をやめる 読者になる 読者になる

Rails Webook

自社のECを開発している会社で働いています。Rails情報やサービスを成長させる方法を書いていきます

RSpec3 / Capybara / Capybara-Webkit チートシート

Rails Test Rails中級 まとめ

Railsの有名なテストフレームワークのRSpec、エンドツーエンドテスト用のフレームワークのCapybara、エンドツーエンドテストのJavascript(Ajaxなど)部分のテスト用のCapybara-webkitのチートシートです。
基本的な使い方を知っていることを前提に、Railsでの使い方をまとめました。

その他参考になるサイトです。

動作確認

  • Rails 4.1.7
  • rspec-rails 3.1.0
  • capybara 2.4.1
  • capybara-webkit 1.3.0

目次

RSpec Matcher

値が等しい(eq, not_to eq)

expect(actual).to     eq expected
expect(actual).not_to eq not_expected

同じオブジェクト(be)

expect(actual).to be expected

# それぞれString型のインスタンスが作成されるため同じオブジェクトではない
expect("test").to be "test" # => Failure/Error

比較(>, >=, <=, <, be_within)

expect(actual).to be >  expected
expect(actual).to be >= expected
expect(actual).to be <= expected
expect(actual).to be <  expected
expect(actual).to be_within(delta).of(expected)

# 例:1.05は1から0.1以内の範囲
expect(1.05).to be_within(0.1).of(1)

正規表現(match)

expect(actual).to match(/expression/)

インスタンスのクラスやタイプタイプ

# あるクラスのインスタンスか確認する
expect(actual).to be_an_instance_of(expected)

# 例:"str"はStringクラスのインスタンス
expect("str").to be_an_instance_of String


# あるクラス階層(継承関係もたどる)のインスタンスか確認する
expect(actual).to be_a  expected
expect(actual).to be_an expected        # be_a のエイリアス
expect(actual).to be_a_kind_of expected # be_a の別のエイリアス

# 例: 1.11は継承関係も含むObjectクラスのインスタンス
expect("1.11").to be_an Object

真偽値、nil

expect(actual).to be true
expect(actual).to be false
expect(actual).to be_nil
expect(actual).to_not be_nil

例外(Exception)

expect { 処理 }.to raise_error
expect { 処理 }.to raise_error ErrorClass
expect { 処理 }.to raise_error "message"
expect { 処理 }.to raise_error(ErrorClass, "message")

# 例:ActiveRecord::RecordNotFound例外が発生することを確認
expect { Product.find(-1) }.to raise_error ActiveRecord::RecordNotFound

predicateマッチャ

アプリ内のxxx?メソッドをRSpec内でbe_xxxMacherとして記載ができる。(例:include? => be_include
また、同様にhas_xxx?メソッドをhave_xxxとして記載ができる。

expect(actual).to be_xxx
expect(actual).to have_xxx(:arg)

範囲(ruby >= 1.9の場合)

expect(1..10).to cover(3)
expect(1..10).not_to cover(-1)

数の変化(change, by, from, to)

# 商品の総数が1増加することを確認
expect {
  Product.create name: "MyProduct"
}.to change { Product.count }.by(1)

# 商品の総数が1増加することを確認
Product.create name: "MyProduct"
expect {
  Product.first.destroy
}.to change { Product.count }.by(-1)

# 商品の総数が1から3個へなったことを確認
Product.create name: "MyProduct"
expect {
  2.times { product.create name: "MyProduct" }
}.to change { Product.count }.from(1).to(3)

配列、文字列に含む

# 含む
expect(actual).to include(expected)

# 開始する、終了する
expect(actual).to start_with(expected)
expect(actual).to end_with(expected)

# 配列がマッチするか確認する
expect(actual).to match_array(expected_array) # 上記と同様

# 例
expect([1, 2, 3]).to include(1)
expect([1, 2, 3]).to include(1, 2)
expect([1, 2, 3]).to start_with(1)
expect([1, 2, 3]).to start_with(1, 2)
expect([1, 2, 3]).to end_with(3)
expect([1, 2, 3]).to end_with(2, 3)
expect({:a => 'b'}).to include(:a => 'b')
expect("this string").to include("is str")
expect("this string").to start_with("this")
expect("this string").to end_with("ring")
expect([1, 2, 3]).to contain_exactly(2, 3, 1)
expect([1, 2, 3]).to match_array([3, 2, 1])


独自RSpec Matcher

RSpec::Matchers.defineで定義します。定義場所は、spec/support配下に個別の名前をつけて作成すると良いでしょう。

# 定義
# spec/support/have_flash_message.rb
RSpec::Matchers.define :have_flash_message do |message, type|
  match do |page|
    expect(page).to have_selector("div.alert.alert-#{type}", text: message)
  end
end

# 利用
expect(page).to have_flash_message '更新に失敗しました', 'danger'


CanCanCanのAbilityの状況を確認するリッチなカスタムマッチャ

# Examples
#
# @user.should have_ability(:create, for: Post.new)
# @user.should have_ability([:create, :read], for: Post.new)
# @user.should have_ability(
#   {create: true, read: false, update: false, destroy: true}, for: Post.new)
RSpec::Matchers.define :have_ability do |ability_hash, options = {}|
  match do |user|
    ability = Ability.new(user)
    target = options[:for]
    @ability_result = {}
    ability_hash = { ability_hash => true } if ability_hash.is_a? Symbol
    ability_hash = ability_hash.reduce({}) { |a, e| a.merge(e => true) } if
      ability_hash.is_a? Array
    ability_hash.each do |action, _true_or_false|
      @ability_result[action] = ability.can?(action, target)
    end
    ability_hash == @ability_result
  end

  failure_message do |user|
    ability_hash, options = expected
    ability_hash = { ability_hash => true } if ability_hash.is_a? Symbol
    ability_hash = ability_hash.reduce({}) { |a, e| a.merge(e => true) } if
      ability_hash.is_a? Array
    target = options[:for]
    "expected User:#{user} to have ability:#{ability_hash} for #{target}, " \
    "but actual result is #{@ability_result}"
  end

  # clean up output of RSpec Documentation format
  description do
    if ability_hash.length == 1
      "have ability #{expected.to_s.match(/(:[^ ]*)/)[1]} " \
      "for #{expected.to_s.match(/<([^ ]*)/)[1]}"
    else
      "have abilities #{expected.to_s.match(/\[(\[[^\]]*\]),/)[1]} " \
      "for #{expected.to_s.match(/<([^ ]*)/)[1]}"
    end
  end
end


RSpec Mock(モック/スタブ)

モック/スタブの基本

RSpecでは、allowreceiveand_returnを使うことで、モックのクラスメソッドやインスタンスメソッドを作成できる。
モック化するメソッドは、空メソッドでも良いのでdefで定義しておく必要がある。

# インスタンスメソッド(product.name)のリターン値を"Mock Name"と定義している
product = build(:product)
allow(product).to receive(:name).and_return("Mock Name")
puts product.name # => "Mock Name"

# and_returnではなく、ブロックでも同様
allow(product).to receive(:name) { "Mock Name" }


# クラスメソッド(Product.count)のリターン値を 5 と定義している
allow(Product).to receive(:count).and_return(5)
puts Product.count # => 5

連続的なリターン値

and_returnに複数値を渡すと、呼び出すたびに値を変更することができる。

allow(Product).to receive(:increment).and_return(1, 2, 3)
puts Product.increment # => 1
puts Product.increment # => 2
puts Product.increment # => 3
puts Product.increment # => 3
puts Product.increment # => 3

例外を返す

allow(double).to receive(:msg).and_raise(error)


Shoulda-Macher

主にモデルのバリデーションのテストを簡単にかける
File: README — Documentation by YARD 0.8.7.3




Capybara

操作 - 遷移する

URL指定("/projects")でも、パス指定(xxx_path)でも指定が可能。

visit "/projects"
visit post_comments_path(post)

操作 - リンクやボタンを押す

click_link   "お問い合わせ" # リンク(aタグ)を押す。リンクテキストかIDを引数にする
click_button "登録する"    # ボタンを押す。ボタンのvalue値(ボタンの表示文字)を引数にする
click_on "登録する"        # ボタンかリンクを押す

操作 - フォーム入力

# テキストフィールド/テキストエリア入力
fill_in "入力フィールド", with: "入力値"  # namd属性、id属性、ラベルで入力フィールドを指定できる

# ラジオボタンを選択
choose  "ラジオボタン"  # namd属性、id属性、ラベルで入力フィールドを指定できる

# チェックボックスのチェックをつける、チェックを外す
check   "チェックボックス" # namd属性、id属性、ラベルで入力フィールドを指定できる
uncheck "チェックボックスを外す"

# ファイルアップロード
# 第一引数は、namd属性、id属性、ラベルで入力フィールドを指定できる
# 第二引数は、ファイルのパスを指定
attach_file "ファイル入力", "/path/to/image.jpg"

# セレクトボックスの選択
# 第一引数は、セレクトするOption属性の値
# 第二引数のfromオプションは、セレクトボックスのid属性, name属性, ラベルを指定する
select  "option", form: "select box"

検証 - 現在のパスを確認

current_pathメソッドにより、現在のパスを検証できます。

expect(current_path).to eq post_comments_path(post)

検証 - 画面表示文字の存在を確認

expect(page).to have_content "foo"
expect(page).to have_no_content "bar"

検証 - CSS、XPathの存在を確認

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

検証 - 特定の値の表示有無を確認

find_link("Hello").visible?
expect(find('#navigation')).to have_button('Sign out')

特定のスコープで操作、検証

whithinにより、入力、検証の範囲を指定することができる。

# CSS, XPathなどで指定することが可能
within "li#employee" do
  fill_in 'Name', :with => 'Jimmy'
end

within :xpath, '//div[contains(., "bar")]' do
  ...
end

within "li", text: "リストテキスト" do
  ...
end

別のウインドウを操作、検証

window_opend_bywithin_windowで別のウインドウの操作、検証を行うことができます。

# いいねボタンを押すと、Facebookのログイン用のウインドウが開かれる
facebook_window = window_opened_by do
  click_button 'いいね'
end

# Facebookのログイン用のウインドウで情報を登録してログインしている
within_window facebook_window do
  find('#login_email').set('a@example.com')
  find('#login_password').set('qwerty')
  click_button 'ログイン'
end

デバッグ

tmp/capybara配下にスナップショットのHTMLファイルを作成する
(CSS適用や入力フィールドが入力されていないので注意)
launchyというgemを入れるとテスト実行時に自動的にブラウザで開いてくれる。

save_and_open_page

画像ファイルとして画面を表示する(CSSや入力フィールドは入力されている)

page.save_screenshot 'screenshot.png'


HTMLのbody要素の中身を確認する

puts page.body     # => HTMLボディの内容 ...


JSのテスト(with Capybara-WebKit)

Javascriptをテストする場合、js: trueを追加する。
rails_helper.rbでの、通常時とJS True時のDatabase Cleanerの削除方式が違うので注意が必要。

Driverがサポートしている場合、モーダルを操作できる。

操作 - JSのalert, confirmダイアログのボタンを押す

CapybaraとCapybara-Webkitを使うことで、Javascriptのalert()confirmで呼ばれたダイアログのボタンを押せます。
Todo alert画像

注意点として、下記のメソッドは結果に関わらず次へ進んでしまうらしいので、メッセージ取得などの結果を確実に取得したい場合はsleepなどを入れる必要がある

# OKボタンを押す
page.driver.browser.accept_js_confirms

# NOボタンを押す
page.driver.browser.dismiss_js_confirms

# メッセージを取得する
page.driver.browser.alert_messages
page.driver.browser.confirm_messages
page.driver.browser.console_messages
  • エラー(404, 500など)のテスト

rescue_fromでエラーを定義した時に、404や500エラーを意図的に発生させたいことがあると思います。
RSpecやCapybaraの機能を使うこと簡単にテストすることができます。

### アプリケーションコード ###
class ApplicationController < ActionController::Base
  protect_from_forgery :with => :exception

  # 例外ハンドル
  # NOTICE: rescue_from は下から評価される
  unless Rails.env.development?
    rescue_from Exception,                        :with => :render_500
    rescue_from ActionController::RoutingError,   :with => :render_404
    rescue_from ActiveRecord::RecordNotFound,     :with => :render_404
  end
end


### テストコード ###
describe "404エラー" do
  context "RoutingError" do
    before do
      visit "/undefined_routing_path" # 存在しないパスにアクセスする
    end

    it "404エラーページに遷移すること" do
      expect(page).to have_content("404 File not found")
    end

    it "ステータスコードが404であること" do
      expect(page.status_code).to eq 404
    end
  end

  context "RecordNotFound" do
    before do
      visit admin_shop_path(1000000) # 存在しないレコードのIDにアクセスする
    end

    it "404エラーページに遷移すること" do
      expect(page).to have_content("404 File not found")
    end

    it "ステータスコードが404であること" do
      expect(page.status_code).to eq 404
    end
  end
end


describe "500エラー" do
  before do
    # 一覧画面に遷移したら例外を発生させる
    expect_any_instance_of(ProductsController).to receive(:index).and_throw(Exception)

    visit products_path
  end

  it "500エラーページに遷移すること" do
    expect(page).to have_content "500 Internal Server Error"
  end

  it "ステータスコードが500であること" do
    expect(page.status_code).to equal(500)
  end
end