Ruby
Rails
RSpec
Capybara

使えるRSpec入門・その4「どんなブラウザ操作も自由自在!逆引きCapybara大辞典」

はじめに

みなさんこんにちは!
この記事は「必要最小限の努力で最大限実戦で使える知識を提供するRSpec入門記事」、略して「使えるRSpec入門」の第4回です。

今回はCapybaraを使ったフィーチャスペックについて説明します。
ただし、今までの記事とは異なり、フィーチャスペックのイロハよりも「Capybaraの使い方」に重点を置きます。

なぜなら、僕個人の経験からいって、フィーチャスペックで困るのは「このブラウザの操作って、どうやってコードで表現するの??」というケースが大半だからです。
それ以外は第1回~第3回の内容をそのまま応用できるので、特に「フィーチャスペックだから困る」ということはないと思います。

今回は説明する主な項目は以下の通りです。

  • フィーチャスペックの基本
  • ページの移動や画面のクリック、フォームの操作など
  • 画面やフォームの検証
  • 画面の操作や検証の応用テクニック
  • その他、知っておくと便利なテクニック

なお、Capybaraの使い方は「逆引き形式」で説明しています。
Qiitaの「目次」を使えば、実現したいブラウザ操作へすぐにジャンプできるはずです。

フィーチャスペックは使いこなせると、とても有益なテストになります。
なぜなら、viewからデータベースまで、すべての動作確認を一つのテストで実現できるからです。
プログラムを変更してもフィーチャスペックがパスしていれば、「この機能はちゃんと使えているはず」という自信が持てます。(過信は禁物ですけどね)

ぜひフィーチャスペックを使いこなせるようになりましょう!

対象となる読者

  • RSpecでフィーチャスペックを一度も書いたことがない人
  • フィーチャスペックは書いているが、Capybaraの細かい機能はよく忘れる人(僕みたいに)

対象となるバージョン

  • Rails 4.2
  • Ruby 2.2
  • rspec-rails 3.1
  • Capybara 2.4
  • Poltergeist 1.5.1

第1回~第3回の記事はもう読みましたか?

第1回では「RSpecの基本的な構文や便利な機能を理解する」というテーマでRSpecの基本を説明しました。
第2回では「使用頻度の高いマッチャを使いこなす」というテーマでマッチャの使い方をいろいろと説明しました。
第3回では「ゼロからわかるモック(mock)を使ったテストの書き方」というテーマでモックの使い方を詳しく説明しました。

今回、「読者のみなさんは第1回と第2回の内容は理解できている」という前提で説明していくので、まだ読んでいない方は先に過去の記事を読んでおいてください。(第3回の内容は特に出てこないので必須ではありません)

それでは始めましょう!

フィーチャスペックの基本

フィーチャスペック(feature spec)はブラウザ上の操作をシミュレートして、実行結果を検証するテストです。
こうしたテストは一般的に「統合テスト」や「エンドツーエンドテスト(E2Eテスト)」と呼ばれるものです。

以下はログイン機能をテストする、フィーチャスペックの単純なサンプルです。

require 'rails_helper'

feature 'ログインとログアウト' do
  background do
    # ユーザを作成する
    User.create!(email: 'foo@example.com', password: '123456')
  end
  scenario 'ログインする' do
    # トップページを開く
    visit root_path
    # ログインフォームにEmailとパスワードを入力する
    fill_in 'Email', with: 'foo@example.com'
    fill_in 'Password', with: '123456'
    # ログインボタンをクリックする
    click_on 'ログイン'
    # ログインに成功したことを検証する
    expect(page).to have_content 'ログインしました'
  end
end

フィーチャスペックのエイリアス

フィーチャスペックでは describeit の代わりに、featurescenario といったエイリアスをよく使います。

フィーチャスペックで使われるエイリアスの対応関係は以下の通りです。

  • describe <=> feature
  • it <=> scenario
  • let <=> given
  • let! <=> given!
  • before <=> background

contextafter にはエイリアスがないのでそのまま使います。

ただし、あくまでエイリアスなので、describeit を使って書いても構いません。

require 'rails_helper'

describe 'ログインとログアウト', type: :feature do
  before do
    # ユーザを作成する
    User.create!(email: 'foo@example.com', password: '123456')
  end
  it 'ログインする' do
    # トップページを開く
    visit root_path
    # ログインフォームにEmailとパスワードを入力する
    fill_in 'Email', with: 'foo@example.com'
    fill_in 'Password', with: '123456'
    # ログインボタンをクリックする
    click_on 'ログイン'
    # ログインに成功したことを検証する
    expect(page).to have_content 'ログインしました'
  end
end

フィーチャスペックのセットアップ

Railsでフィーチャスペックを書く場合は rspec-rails や Capybara といったgemのインストールや、設定ファイルの作成が必要になります。

詳しい方法は以下の記事を参考にしてみてください。
(ただしバージョンがちょっと古くなっているので、バージョン番号は適宜最新のものを指定してください)

初心者大歓迎!RSpec 3でドラッグアンドドロップ機能をテストする方法(スクリーンキャスト付き)

それでは、次のセクション以降でCapybaraの使い方を逆引き形式で説明していきます。
こうした機能を組み合わせれば、フィーチャスペックを自由自在に書けるはずです。

ページの移動や画面のクリック、フォームの操作など

特定のページへ移動する

visit + path で特定のページに移動できます。

visit root_path
visit new_user_path

現在のページを再読み込み(リロード)する

Capybaraの標準APIにはリロード用のメソッドが無いので、「現在のパスに再度移動する」という操作でリロードをシミュレートします。

visit current_path

ボタンをクリックする

「ボタン」というのはsubmitボタンやbuttonタグを指します。

<input type="submit" name="commit" value="Save">
<!-- または -->
<!-- <button>Save</button> -->
click_button 'Save'

リンクをクリックする

CSSによって見た目がボタンっぽくなっていても、HTML上はaタグになっている場合は「リンク」と見なされるので注意が必要です。

<a href="/users/new">New User</a>
click_link 'New User'

リンクまたはボタンをクリックする

クリックする要素がリンクなのかボタンなのか、いちいち考えたくない場合は click_on が便利です。

<a href="/users/new">New User</a>
<input type="submit" name="commit" value="Save">
click_on 'New User'
click_on 'Save'

画像のalt属性を利用してリンクをクリックする

<a href="/users/1">
  <img alt="Alice" src="./profile.jpg">
</a>

<!-- buttonの場合はNG -->
<!-- <button> -->
<!--   <img alt="Alice" src="./profile.jpg"> -->
<!-- </button> -->
click_on 'Alice'
# または
# click_link 'Alice'

テキストボックスまたはテキストエリアに文字を入力する

<label for="blog_title">タイトル</label>
<input type="text" value="" name="blog[title]" id="blog_title">
fill_in 'タイトル', with: 'あけましておめでとうございます。'

セレクトボックスを選択する

<label for="japanese_calendar">和暦</label>
<select name="japanese_calendar" id="japanese_calendar">
  <option value="0">明治</option>
  <option value="1">大正</option>
  <option value="2">昭和</option>
  <option value="3">平成</option>
</select>
select '平成', from: '和暦'

チェックボックスのチェックをON/OFFする

<label for="mailmagazine">
    <input type="checkbox" name="mailmagazine" id="mailmagazine" value="1">
    メールマガジンを購読する
</label>
check 'メールマガジンを購読する'
uncheck 'メールマガジンを購読する'

ラジオボタンを選択する

<label>
  <input id="user_sex_male" name="user[sex]" type="radio" value="male" checked="checked">
  男性
</label>
<label>
  <input id="user_sex_female" name="user[sex]" type="radio" value="female">
  女性
</label>
choose '女性'

ファイルを添付する

"#{Rails.root}/spec/factories/profile_image.jpg" の部分は実際にファイル配置しているパスを指定してください。

attach_file 'プロフィール画像', "#{Rails.root}/spec/factories/profile_image.jpg"

hiddenに値をセットする

<input type="hidden" name="secret_value" id="secret_value">

find を普通に使うと画面に表示されている要素しか検索しないので、 visible: false オプションを付けます。

find('#secret_value', visible: false).set('secret!!')

ラベルがないフィールドを操作する

ラベルの代わりにid、name、placeholderを指定できます。

テキストボックスの場合

<input type="text" name="blog[title]" id="blog_title" value="" placeholder="ここにタイトルを入力します">
fill_in 'blog_title', with: 'あけましておめでとうございます。'
# または
# fill_in 'blog[title]', with: 'あけましておめでとうございます。'
# fill_in 'ここにタイトルを入力します', with: 'あけましておめでとうございます。'

セレクトボックスの場合

<select name="japanese_calendar" id="japanese_calendar">
  <option value="0">明治</option>
  <option value="1">大正</option>
  <option value="2">昭和</option>
  <option value="3">平成</option>
</select>
select '平成', from: 'japanese_calendar'

チェックボックスの場合

<input type="checkbox" name="mailmagazine" id="mailmagazine" value="1">
check 'mailmagazine'

画面やフォームの検証

ページ内に特定の文字列が表示されていることを検証する

<div class="alert alert-success">
  ユーザーが作成されました。
</div>
expect(page).to have_content 'ユーザーが作成されました。'

特定のタグやCSS要素に特定の文字列が表示されていることを検証する

<h1 class="information" id="information">大事なお知らせ</h1>
expect(page).to have_selector 'h1', text: '大事なお知らせ'

タグではなく、CSSクラスやIDを使うこともできます。

expect(page).to have_selector '.information', text: '大事なお知らせ'
# または
expect(page).to have_selector '#information', text: '大事なお知らせ'
# タグとクラスを組み合わせるのも可
expect(page).to have_selector 'h1.information', text: '大事なお知らせ'

タグの属性も指定することができます。

<a href="contacts/1" data-method="delete">delete</a>
expect(page).to have_selector 'a[data-method=delete]', text: 'delete'

text には正規表現を渡すこともできます。

expect(page).to have_selector 'h1', text: /^大事なお知らせ$/

ページ内に特定のボタンが表示されていることを検証する

<input type="submit" name="commit" value="Save">
expect(page).to have_button 'Save'

ページ内に特定のリンクが表示されていることを検証する

<a href="/users/sign_up">会員登録はこちら</a>
expect(page).to have_link '会員登録はこちら'

ページ内に特定のCSS要素が表示されていることを検証する

<!-- バリデーションエラー時のHTML -->
<div class="field_with_errors">
  <input type="text" value="" name="blog[title]" id="blog_title">
</div>
# ブログの新規作成画面を開く
visit new_blog_path
# 何も入力せずに作成ボタンをクリックする
click_on 'Create Blog'
# フォーム内の要素にfield_with_errorsクラスが付いていることを検証する
expect(page).to have_css '.field_with_errors'

テキストボックスに特定の値が入っていることを検証する

<!-- ブログの編集画面を開いたときのHTML -->
<label for="blog_title">タイトル</label>
<input type="text" value="あけましておめでとうございます。" name="blog[title]" id="blog_title">
# ブログの編集画面を開く
click_link 'Edit'
# 編集前のタイトルが表示されていることを検証する
expect(page).to have_field 'タイトル', with: 'あけましておめでとうございます。'

チェックボックスがチェックされていること/いないことを検証する

<label>
    <input type="checkbox" name="mailmagazine" id="mailmagazine" value="yes" checked="checked">
    メールマガジンを購読する
</label>
expect(page).to have_checked_field('メールマガジンを購読する')

チェックされて いないこと を検証する場合は have_unchecked_field を使います。

expect(page).to have_unchecked_field('メールマガジンを購読する')

チェックボックスが表示されていることを検証する(チェックの有無は無視する)

チェックボックスが表示されていることだけを検証したい、チェックの有無は気にしない、という場合は have_field を使います。

<label>
    <input type="checkbox" name="mailmagazine" id="mailmagazine" value="yes" checked="checked">
    メールマガジンを購読する
</label>
# チェックが付いていてもいなくてもパスする 
expect(page).to have_field('メールマガジンを購読する')

セレクトボックスで特定の項目が選択されていることを検証する

<label for="japanese_calendar">和暦</label>
<select name="japanese_calendar" id="japanese_calendar">
  <option value="明治">明治</option>
  <option value="大正">大正</option>
  <option selected="selected" value="昭和">昭和</option>
  <option value="平成">平成</option>
</select>
expect(page).to have_select('和暦', selected: '昭和')

セレクトボックスで特定の項目が存在することを検証する

<label for="japanese_calendar">和暦</label>
<select name="japanese_calendar" id="japanese_calendar">
  <option value="明治">明治</option>
  <option value="大正">大正</option>
  <option value="昭和">昭和</option>
  <option value="平成">平成</option>
</select>
expect(page).to have_select('和暦', options: ['明治', '大正', '昭和', '平成'])

selectedと一緒に検証することもできます。

expect(page).to have_select('和暦', selected: '昭和', options: ['明治', '大正', '昭和', '平成'])

この検証方法はこちらの記事で実際の使用例を見ることができます。

(応用) セレクトボックスでselectedになっている項目がないことを検証する

以下のように selected になっている項目がないセレクトボックスを想定します。

<label for="japanese_calendar">和暦</label>
<select name="japanese_calendar" id="japanese_calendar">
  <option value="meiji">明治</option>
  <option value="taisho">大正</option>
  <option value="showa">昭和</option>
  <option value="heisei">平成</option>
</select>

画面上は一番先頭にある "明治" が表示されていますが、だからといって次のようなコードを書くと検証に失敗します。("明治"は selected になっていないため)

# NG:検証に失敗する
expect(page).to have_select('和暦', selected: '明治')

「どれも selected ではないこと」をスマートに検証できるメソッドはないので、もしやるとすれば次のようにループを回して全項目が selected でないことを検証する必要があります。

within find_field('和暦') do
  all('option').each do |option|
    expect(option['selected']).to be_blank
  end
end

なお、selectedになっていなくても、valueメソッドを使うと画面上に表示されている項目のvalueを取得できます。

# selected になっていなくても、画面上に表示されている項目のvalueは取得できる 
expect(find_field('和暦').value).to eq 'meiji'

ラジオボタンで特定の項目が選択されていることを検証する

<label>
  <input id="user_sex_male" name="user[sex]" type="radio" value="male" checked="checked">
  男性
</label>
<label>
  <input id="user_sex_female" name="user[sex]" type="radio" value="female">
  女性
</label>
expect(page).to have_checked_field('男性')

titleタグの文言を検証する

<html>
<head>
<title>My favorite songs</title>
...
expect(page).to have_title 'My favorite songs'

「表示されていないこと」や「選択されていないこと」を検証する

以下は画面に特定の文字列が表示されていないことを検証するコードです。

expect(page).to_not have_content '秘密のパスワード'

上のコードは次のように to + have_no_content を使って書くこともできます。

expect(page).to have_no_content '秘密のパスワード'

他にも have_no_xxx の形式で検証できる要素があります。

  • have_no_button
  • have_no_checked_field
  • have_no_css
  • have_no_field
  • have_no_link
  • have_no_select
  • have_no_selector
  • have_no_table
  • have_no_text
  • have_no_title
  • have_no_unchecked_field
  • have_no_xpath

hidden項目に設定されている値を検証する

<input type="hidden" name="secret_value" id="secret_value" value="secret!!">

find を普通に使うと画面に表示されている要素しか検索しないので、 visible: false オプションを付けます。

expect(find('#secret_value', visible: false).value).to eq 'secret!!'

フォームの要素がdisabledになっていることを検証する

<label for="blog_title">タイトル</label>
<input type="text" value="" name="blog[title]" id="blog_title" disabled="disabled">

disabledでなければ find_field('タイトル') のように書けますが、disabledだと使えないので、代わりに find + id を使います。

expect(find('#blog_title')).to be_disabled

(2015.10.22 追記)disabledオプションを使って検証する

以下のように disabled: true を指定すれば disabled になっているコントロールも検証できることがわかりました。
こちらの方が自然で理解しやすいと思います。

expect(page).to have_field 'タイトル', disabled: true 

テキストボックスだけでなく、チェックボックスやボタン等でも使えます。

expect(page).to have_checked_field 'メールを受け取る', disabled: true

expect(page).to have_button '更新する', disabled: true 

特定のCSS要素が非表示になっていることを検証する

<div style="display: none;" class="secret-message">
  よい子は見ちゃダメ
</div>

find を普通に使うと画面に表示されている要素しか検索しないので、 visible: false オプションを付けます。

expect(find('.secret-message', visible: false)).to_not be_visible

タグの属性値を検証する

<input type="submit" value="Create User" data-confirm="Are you sure?">
button = find_button 'Create User'
expect(button['data-confirm']).to eq 'Are you sure?'

現在のページが特定のパスであることを検証する

click_on 'New User'
expect(current_path).to eq new_user_path

レスポンスのステータスコードを検証する

click_on 'Create User'
expect(page).to have_http_status(:success)
# または
# expect(page).to have_http_status(:ok)
# expect(page).to have_http_status(200)

その他のパターンについては公式ドキュメントを参照してください。

https://www.relishapp.com/rspec/rspec-rails/docs/matchers/have-http-status-matcher

画面の操作や検証の応用テクニック

文字列で指定できない(または指定しにくい)要素を操作する

リンクにアイコンしか表示されていない場合など、画面に表示されている文字列を指定できない(または指定しにくい)場合は、find + id/class + action で操作します。

以下は class でリンクを指定し、そのリンクをクリックする例です。

<a class="settings-link" href="/settings">
  <i class="fa fa-gears"></i>
</a>
find('.settings-link').click

id で指定する場合は、ドット(.)ではなくシャープ(#)で指定します。

<a id="settings-link" href="/settings">
  <i class="fa fa-gears"></i>
</a>
find('#settings-link').click

findと組み合わせられるアクションはクリックだけでなく、find('#some-textbox').set('Hello')find('#some-checkbox').set(true) のようにフォームに値をセットしたりすることもできます。

詳しくはAPIドキュメントを参照してください。

http://www.rubydoc.info/github/jnicklas/capybara/Capybara/Node/Element

文字列で指定できない(または指定しにくい)要素を検証する

リンクにアイコンしか表示されていない場合など、画面に表示されている文字列を指定できない(または指定しにくい)場合は、find + id/class + プロパティ で検証します。

以下は class でリンクを指定し、そのリンクのhref属性を検証する例です。

<a class="settings-link" href="/settings">
  <i class="fa fa-gears"></i>
</a>
link = find('.settings-link')
expect(link[:href]).to eq edit_user_path

id で指定する場合は、ドット(.)ではなくシャープ(#)で指定します。

<a id="settings-link" href="/settings">
  <i class="fa fa-gears"></i>
</a>
link = find('#settings-link')
expect(link[:href]).to eq edit_user_path

find + タグ名と、タグ内のテキストを組み合わせて取得することも可能です。

<a>About Ruby</a>
# href属性がないと click_link 'About Ruby' ではクリックできないため、以下のようにする
link = find('a', text: 'About Ruby')
link.click

find と組み合わせて取得できる値はタグの属性だけでなく、find('#some-textbox').valuefind('#some-checkbox').checked? のようにフォームの値を取得したりすることもできます。

詳しくはAPIドキュメントを参照してください。

http://www.rubydoc.info/github/jnicklas/capybara/Capybara/Node/Element

画面上に複数現れる要素を絞り込む

Capybaraは指定された条件に一致する要素が複数見つかるとエラーを投げます。

たとえば、以下の例では "はい" と "いいえ" のラジオボタンが2つずつあるので、単純に選択しようとするとエラーが起きます。(どちらの "はい" なのか決められないため)

<div class="section-drug">
  現在薬を飲んでいますか?
  <label>
    <input id="user_drug_yes" name="user[drug]" type="radio" value="yes" checked="checked">
    はい
  </label>
  <label>
    <input id="user_drug_no" name="user[drug]" type="radio" value="no">
    いいえ
  </label>
</div>

<div class="section-disease">
  大きな病気にかかったことはありますか?
  <label>
    <input id="user_disease_yes" name="user[disease]" type="radio" value="yes" checked="checked">
    はい
  </label>
  <label>
    <input id="user_disease_no" name="user[disease]" type="radio" value="no">
    いいえ
  </label>
</div>
choose 'はい'
# => Capybara::Ambiguous: Ambiguous match, found 2 elements matching radio button "はい"

このような場合は within を使ってスコープを絞ってから操作を実行します。

within '.section-drug' do
  choose 'はい'
end

within '.section-disease' do
  choose 'いいえ'
end

Tips:テストしにくいviewはテストをしやすいように変更する

within で絞り込める条件がない(上の例だと、section-drugsection-disease のdivがない)場合は、テストしやすいようにidやclassを付けることを検討してください。

条件に合致する要素を配列として取得する

下のように、3件のデータが表示されているテーブルがあったとします。

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Mozell Conn</td>
      <td>
        <a href="/contacts/1/edit">Edit</a>
      </td>
    </tr>
    <tr>
      <td>Adaline Gleason</td>
      <td>
        <a href="/contacts/2/edit">Edit</a>
      </td>
    </tr>
    <tr>
      <td>Jazmyne Goldner</td>
      <td>
        <a href="/contacts/3/edit">Edit</a>
      </td>
    </tr>
  </tbody>
</table>

このテーブルの中にある2行目の Edit リンクをクリックしたい、という場合は以下のように all メソッドを活用します。

all('tbody tr')[1].click_link 'Edit'

all('tbody tr') は「tbody タグ内に存在する、すべての tr タグを選択する」を意味します。

all メソッドでは条件に合致した要素の配列が返ってくるので、2行目を選択したい場合は [1] とすれば、2行目の tr タグだけにフォーカスできます。

あとは「この tr タグ内に存在する Edit リンクをクリックする」という意味で click_link 'Edit' と書けば、2行目に表示されている Edit リンクをクリックできます。

最初に出現する要素を取得する

たとえば以下のように、最初に出現する tr タグを取得するコードがあったとします。

all('tbody tr')[0].click_link 'Edit'

これは first を使うと簡潔に書けます。

first('tbody tr').click_link 'Edit'

応用テクニックして、 within と組み合わせて使うこともできます。

within first('tbody tr') do
  click_link 'Edit'
end

ちなみに、 lastsecond のようなショートカットメソッドはないみたいです。
all('tbody tr').lastall('tbody tr')[1] のように書いてください。

参考: http://www.rubydoc.info/github/jnicklas/capybara/Capybara/Node/Finders

特定のパスへのリンクを検証する/クリックする

ページ内に同じようなリンクが複数あり、特定のリンクだけにフォーカスしたい場合は具体的なパス(aタグのhref属性)で絞り込むこともできます。

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Mozell Conn</td>
      <td>
        <a href="/contacts/1/edit">Edit</a>
      </td>
    </tr>
    <tr>
      <td>Adaline Gleason</td>
      <td>
        <a href="/contacts/2/edit">Edit</a>
      </td>
    </tr>
    <tr>
      <td>Jazmyne Goldner</td>
      <td>
        <a href="/contacts/3/edit">Edit</a>
      </td>
    </tr>
  </tbody>
</table>
contact = Contact.find_by(name: 'Adaline Gleason')

expect(page).to have_link 'Edit', href: edit_contact_path(contact)

click_link 'Edit', href: edit_contact_path(contact)

リンク文字列がない場合(アイコンが表示されている場合など)はhrefだけで絞り込むこともできます。

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Mozell Conn</td>
      <td>
        <a href="/contacts/1/edit">
          <i class="fa fa-pencil"></i>
        </a>
      </td>
    </tr>
    <tr>
      <td>Adaline Gleason</td>
      <td>
        <a href="/contacts/2/edit">
          <i class="fa fa-pencil"></i>
        </a>
      </td>
    </tr>
    <tr>
      <td>Jazmyne Goldner</td>
      <td>        
        <a href="/contacts/3/edit">
          <i class="fa fa-pencil"></i>
        </a>
      </td>
    </tr>
  </tbody>
</table>
contact = Contact.find_by(name: 'Adaline Gleason')

expect(page).to have_link nil, href: edit_contact_path(contact)

click_link nil, href: edit_contact_path(contact)

その他、知っておくと便利なテクニック

テストが失敗する直前の画面をブラウザで開く

以下のように、なぜか失敗するテストがあったとします。

click_on 'Create User'
expect(page).to have_content 'ユーザを作成しました'
# エラー => expected to find text "ユーザを作成しました" in "...

失敗する原因がすぐにわからない場合は save_and_open_page メソッドを使うと、ブラウザが自動的に起動し、その時点の画面を確認できます。
(ただし、CSSは適用されないので見た目は不格好になります)

click_on 'Create User'
# ブラウザで画面を開く
save_and_open_page
expect(page).to have_content 'ユーザを作成しました'

ブラウザの自動起動には launchy gem が必要

"Please install the launchy gem to open the file automatically." という警告が出てブラウザが起動しない場合は、launchy gemをインストールしてください。

# Gemfile
group :test do 
  gem 'launchy'
end
bundle install

テスト失敗時に自動的に save_and_open_page を呼ぶ

テスト失敗時に自動的に save_and_open_page が呼ばれるようにしておくと、効率よくデバッグできるかもしれません。
詳しくは以下の記事を読んでみてください。

フィーチャスペックでテスト失敗時に自動的に save_and_open_page を実行する方法

手っ取り早くテストをskipする

scenarioxscenario に変更するとそのテストはskipされて実行されなくなります。(pending扱いになります)

xscenario '一時的に実行させたくないシナリオ' do
  # ...
end

JavaScriptを使わないと操作できない処理をテストする

Ajaxを使った処理や、クリックイベントによるモーダル画面の表示など、JavaScriptなしでは実行できない操作がある場合は js: true タグを付けます。

scenario 'Userの作成', js: true do
# または
# it 'Userの作成', js: true do

JavaScriptドライバのセットアップ方法について

JavaScriptを実行する場合は、PoltergeistのようなJavaScriptドライバの設定が必要になります。
具体的な手順は以下の記事を参照してください。

初心者大歓迎!RSpec 3でドラッグアンドドロップ機能をテストする方法(スクリーンキャスト付き)

テストが失敗する場合はsleep等でテストを停止させてみる

JavaScriptの処理が完了しないうちにテストが先に進んでしまうとテストが失敗します。
その場合は sleep を挟むなどして、JavaScriptの処理が終わるまでテストを停止させてください。

# Ajaxでデータを保存するテストの例
expect { 
  click_link 'Save'
  sleep 0.5 # 秒数は適当に調整 
}.to change { Item.count }.by(1)

このあたりのテクニックは sleep 以外を使う方法もいろいろあるので、必要に応じてネットの情報等を参考にしてください。
(とはいえ、僕はたいてい sleep を使っているんですが)

2015.5.10 追記:sleepの代わりにfindやhave_xxxを使う方がベターです

JavaScriptを使う操作の後に画面上の表示が変化する場合はfindhave_xxxを使うとCapybaraが自動的に処理が完了するまで(=画面に変化が現れるまで)待ってくれます。(デフォルトの待ち時間は最大2秒間です)

expect { 
  click_link 'Save'
  # sleepの代わりにfindで待つ
  find '.alert', text: '保存しました' 
}.to change { Item.count }.by(1)

# または
click_link 'Save'
# sleepの代わりにhave_selectorで待つ
expect(page).to have_selector '.alert', text: '保存しました'
expect(Item.count).to eq 1 

詳しくは @syossan27 さんのこちらの記事を参考にしてください。

RSpecのsleep処理について

なぜかときどき落ちるテストでリトライする

自動化テストは百発百中で毎回すべてパスするのが理想的ですが、現実的には「よくわからないがいくつかのテストが時々落ちる」ということが起きたりします。
僕の経験上、特に js: true を付けたフィーチャスペックでよく起きる印象があります。

どうしても原因を追及できない場合は、「何度かリトライしてパスすればOK、何度やってもダメならNG」と見なすのもアリです。

テストをリトライしたい場合は rspec-retry というgemを使うと便利です。

group :test do
  # ...
  gem 'rspec-retry'
end
rails_helper.rb
# ...
require 'rspec/retry'

RSpec.configure do |config|
  # ...

  # 実行中にリトライのステータスを表示する
  config.verbose_retry = true
  # リトライの原因となった例外を表示する
  config.display_try_failure_messages = true

  # js: true のフィーチャスペックのみリトライを有効にする
  config.around :each, :js do |ex|
    ex.run_with_retry retry: 3
  end
end

その他、詳しい使い方はgemのREADMEを参照してください。

https://github.com/NoRedInk/rspec-retry

マウスオーバー(hover)している間しか表示されない要素をクリックする

マウスオーバー(hover)を再現したい場合は hover メソッドを使います。
js: true を付けている点にも注意してください。

scenario '秘密の隠しボタンをクリックする', js: true do
  visit secret_path
  find('.secret-area').hover
  # hover中しか表示されないボタンをクリックする
  find('.secret-button').click
end

リターンキーの押下をシミュレートする

以下の画像のように検索実行ボタンがなく、検索フィールド内でリターンキーを押して検索を実行するフォームを想定します。

Screen Shot 2016-05-07 at 14.13.43.png

<form (略)>
<input type="text" id="search-text">
</form>

リターンキーのシミュレートは js: true でJavaScriptを有効にした場合しか実行できません。
以下はJSドライバとしてPoltergeistを使った場合の実行例です。(Poltergeist以外では動作確認していません)

# 検索テキストを入力
fill_in 'search-text', with: 'Capybara'
# テキストボックス内でリターンキーを押下
find('#search-text').native.send_key(:Enter)
# 検索結果の検証
expect(page).to have_content 'Capybaraの検索結果'

HTMLソースを取得する

たとえばHTMLタグのサニタイズが適切に行われているかどうかを検証するために、HTMLソースを直接参照したいケースがあります。
以下はページ全体のHTMLを取得するコード例です。

visit root_path
html = page.html
# 取得結果 
# <!DOCTYPE html>
# <html>
# <head>
# ...

特定の要素だけにフォーカスする場合は以下のようにします。

visit root_path

# <div class="navbar">内のHTMLを取得する

# js: true を使わない場合
html = find('.navbar').native.inner_html

# js: true を使う場合 (Poltergeist 1.8以上で有効。Poltergeist以外では動作未確認)
html = find('.navbar')['innerHTML']

User-Agentを偽装する

以下はUser-Agentを iPhone 6(iOS 8.1)に偽装する例です。
テストを実行するドライバによって設定方法が異なるので注意してください。

Capybara単体で実行する場合

user_agent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) CriOS/38.0.2125.67 Mobile/12B411 Safari/600.1.4'

Capybara.current_session.driver.header('User-Agent', user_agent)

Capybara + Poltergeistで実行する場合

user_agent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) CriOS/38.0.2125.67 Mobile/12B411 Safari/600.1.4'

page.driver.headers = { "User-Agent" => user_agent }

プログラミング的にログイン状態を作る/OAuth認証を偽装する

DeviseやOmniAuthなど、メジャーな認証系のgemは、たいていテストを考慮した機能を持っています。
そうした機能を使うとテストコード側でプログラミング的にログイン状態を作ったり、OAuth認証を偽装したりすることができます。

# Deviseでプログラミング的にログイン状態を作る
user = User.create(email: 'foo@example.com')
login_as user, scope: :user
visit edit_settings_path

# OmniAuthでTwitter認証を偽装する
OmniAuth.config.test_mode = true
params = { provider: 'twitter', uid: '123545' }
OmniAuth.config.mock_auth[:twitter] = OmniAuth::AuthHash.new(params)
click_on 'Twitter Login'

詳しい使い方は公式Wikiページなど、ネットの情報を参考にしてください。

外部APIと連携する機能をテストする

TwitterやFacebook等のAPIと連携する場合、テストで本当に通信を発生させるのは得策ではありません。
テストが遅くなったり、本物のアカウントが必要になったりして、「いつでも、どこでも、誰でも、何回でもテストを実行できる」ということが困難になるからです。

そこで、こういうケースではモックを使ってAPI連携部分を偽装します。

モックの使い方は以下の記事で詳しく説明しているので、ここでは割愛します。
モックに詳しくない方はぜひ参考にしてみてください。

使えるRSpec入門・その3「ゼロからわかるモック(mock)を使ったテストの書き方」

また、別のアプローチとして、VCRというgemを使う方法もあります。
VCRは最初の1回だけ本当に通信を発生&記録して、2回目以降は記録したデータをモック代わりに使うという、ちょっと変わったgemです。

VCRの使い方はネットの情報や、後述する「Everyday Rails - RSpecによるRailsテスト入門」の第10章を参考にしてみてください。

新しいウインドウ(新しいタブ)の内容を検証する

<a href="/some_information" target="_blank">新しいウインドウを開く</a>
scenario '新しいウインドウの内容を検証する', js: true do
  visit some_path
  click_on '新しいウインドウを開く'
  handle = page.driver.browser.window_handles.last
  page.driver.browser.within_window(handle) do
    expect(page).to have_content '新しいウインドウの内容'
  end
end

このコードを動かす際は以下の点に注意してください。

  • Capybaraのデフォルトドライバではこのテクニックが使えません。必ず js: true を付けてください。
  • 上記のコードはCapybara(2.4.4)、Poltergeist(1.5.1)の環境で動作確認しました。JavaScriptドライバやgemのバージョンが変わると動作しない恐れがあります。(まだAPIが安定していない気がします)

上記のコードは@tyn-iMarketさんの以下の記事を参考にさせてもらいました。どうもありがとうございました。

CapybaraのPoltergeistを使ったテストでブラウザの別のタブに移動する

さらにさらに・・・??

「あ、これも使えそう」と思うようなテクニックが見つかったらまた追記していきます。
いいネタを仕入れたときはQiitaの「通知」でお知らせしますので、よかったらストックしていってください!

まとめ

というわけで、今回はCapybaraを使ったブラウザ操作の方法を逆引き形式で説明してみました。

Capybaraには他にもいろんな機能がありますし、同じ操作を別の方法で実現する方法もあります。
ただし、僕の経験上は今回紹介した機能で9割以上のフィーチャスペックをカバーできています。
よっぽど凝ったUI操作でない限り、今回紹介した機能でほぼ十分フィーチャスペックが書けるはずです。

まだフィーチャスペックを書いたことがない人は、簡単なテストでいいので今回の記事を参考にしながら一度フィーチャスペックを書いてみてください。

「使えるRSpec入門」シリーズは今回の記事で一応おしまいです。
RSpecに抵抗があった人もこの連載を読んで、「あ、なんか使えそう」って思ってもらえると嬉しいです。

Railsをメインで開発している人は続編としてぜひ 「Everyday Rails - RSpecによるRailsテスト入門」 を読んでみてください。
本書では今回説明したフィーチャスペックを含めて、「Railsではどういうふうにテストを書けば良いのか」を実践的なサンプルを使って説明しています。

Everyday Rails - RSpecによるRailsテスト入門
Everyday Rails

また、今後もときどきQiitaやブログにRubyやRails、RSpecに関するお役立ち情報を書いていくつもりなので、よかったらQiitaやTwitterのフォローもお願いします。

それではここまで読んでくれてどうもありがとうございました!
またお会いしましょう~。

参考文献