hiyoko-programingの日記

プログラミングを勉強したてのひよっ子。   エンジニア目指して勉強中。

統合テスト

Webアプリケーションの統合テスト

モデルの単体での機能のテストであったり、コントローラの特定のアクションのテストを書く方法までで記述してきたのはあくまで単体テストである。

特定の機能・バリデーションなど、単独では予測した仕様になっていることはテストできているが、それらを組み合わせて利用した際に、意図した仕様になっているかどうかは確かめられていない。

「ログインフォームにメールアドレスとパスワードを入力してボタンを押すとログインできる」「メニューから必要な情報をクリックするととデータベースの情報が変更される」といった仕様は、モデル・コントローラのアクションという単位ではテストできているが、ブラウザ上で該当する操作を行なった時に、期待する動作をしているかどうかはまだテストできていない。

このような「ユーザーが実際にアプリーションを操作する様子を再現して行われるテストは統合テストと呼ばれる。Rspecを用いて統合テストを書く手段として、フィーチャスペックを利用できる。

フィーチャスペック

フィーチャスペック

フィーチャスペックは、Rspecを使って統合テストを行うためのスペック。テスト環境用の仮想ブラウザを操作して、「特定のa要素をクリックする」「ボタンと対応するコントローラのアクションが動く」といった複雑なテストを書くことができる。細かい機能のテストは既に単体テストで実現しており、アプリケーションの肝となる動作を総合的にテストしたい場合に用いられる。

フィーチャスペックを書くためには、追加でgemを導入する必要がある。
今回はCapybaraというgemを導入して、フィーチャスペックを書いていく。

Capybara

Capybaraは、ブラウザの操作を再現するのに必要なgem。特定の要素をクリックしたり、フォームに値を入力したり、特定の要素が画面に表示されているかなど、様々なブラウザ上の動きをテストすることができる。Capybaraを導入すると、テストを記述するためのヘルパーメソッドが複数追加され、これらを活用してフィーチャスペックを記述していくことになる。以下、代表的なものをいくつか。

visitメソッド

visitメソッドは引数にURL、 もしくはプレフィックスを指定することで、そのページに移動することができるメソッド。大抵の場合、テストしたいHTML要素・機能のあるページに移動してから他のテスト用の動作を書くことになるため、visitメソッドは重宝する。

サンプル
1
2
3
4
5
# "/tweets"に移動する
visit("/tweets")

# tweet_pathに移動する
visit tweet_path

click_onメソッド

click_onメソッドは、指定したHTML要素をクリックするメソッド。引数にはHTML要素のvalue属性を指定する。

サンプル
1
2
3
4
5
# <input type="submit" name="commit" value="こんにちは">という要素をクリック
click_on("こんにちは")

# <a href="/users/new">会員登録する</a>という要素をクリック
click_on("会員登録する")

visitメソッドとclick_onメソッドは特によく使用する。
使用するメソッドの違いの他にも、フィーチャスペックには他のテストにはない2つの特徴がある。

(1)一部の記述の名前が単体テストと異なる

フィーチャスペックでは、itの代わりにscenarioを、beforeの代わりにbackgroundと記述する。他にもdescribefeatureと記述したり、letgivenと記述するなどの慣習がある。

これらは全てエイリアスメソッドなので、名前が異なるだけで内部的な実装は全く同じ。また、フィーチャスペックでもit・before・describe・letを使うことはできる。フィーチャスペックらしい書き方をするか、他のスペックと同じ書き方をするかは、現場のコーディング規約に任せて柔軟に対応する。

サンプル
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# backgroundはbeforeのエイリアスメソッド
background do
  user = create(:user)
  sign_in user
  create(:guest)
end

# scenarioはitのエイリアスメソッド
scenario 'update user' do
  user = User.last
  expect(User.find(user.id).student).to eq true
  expect(User.find(user.id).ticket_count).to eq 2
end

(2)複数のexpectが同一テスト内に記述される

フィーチャスペックは統合テストなので、1つの動作の結果として、複数のexpectを期待する場合がある。

例えば、ユーザーの更新ボタンを押した時に、「ユーザーの情報が更新されていること」「ユーザーの個人情報を別途管理しているプロフィールテーブルも更新されること」「更新後に遷移するページに特定のidのhtml要素があること」を確かめたい場合などが考えられる。フィーチャスペックを書く際には、1つのテストにつき1つのexpectという考え方ではなく、確かめたいタイミングで何個でもexpectを書くようにする。

サンプル
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#1つのscenario(it)の内部に、複数のexpectが記述されている
scenario 'update user' do
  user = User.last
  expect(user).to be_valid
  visit current_user_path
  click_on('更新する', visible: false)
  user.reload
  expect(user.female?).to eq true
  expect(user.letters).to eq 2
end

Pictweetの統合テスト

Pictweetを使ってフィーチャスペックを書いてみる。

Capybaraを導入する

Gemfile
1
2
3
group :test, :development do 
  gem 'capybara'
end
ターミナル
1
$ bundle install
spec/rails_helper.rb
1
2
# 次の記述を追加
require 'capybara/rspec'

これで、Capybaraを使用する準備ができた。

 PicTweetのコードを書き換える

以前作成した状態では、Capybaraがエラーを起こしてしまうため一部分コードを変更する。

変更内容は、imageとtextの入力フォームにidを追加。

以下のどちらに該当するか確認してから変更する。

 app/views/tweets/new.html.erbのヘルパーメソッド を確認して、以下のどちらに当てはまるかチェックする。

form_tagを使用している場合

new.html.erb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<div class="contents row">
  <div class="container">
    <%= form_tag('/tweets', method: :post) do %>
      <h3>
        投稿する
      </h3>
      <input type="text" name="image" placeholder="Image Url" id="image">
      <textarea name="text" placeholder="text" rows="10" cols="30" id="text"></textarea>
      <input type="submit" value="SEND">
    <% end %>
  </div>
</div>

form_withを使用している場合

new.htmlerb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<div class="contents row">
  <%= form_with(model: @tweet, local: true) do |form| %>
    <h3>
      投稿する
    </h3>
    <%= form.text_field :image, placeholder: "Image Url", id: "image" %>
    <%= form.text_area :text, placeholder: "text" , rows: "10", id: "text" %>
    <%= form.submit "SEND" %>
  <% end %>
</div>

作業ファイルを作成

フィーチャスペックを記述するためのファイルを作成する。spec以下にfeaturesというディレクトリを作成し、そこにtweet_spec.rbを作成する。

spec/features/tweet_spec.rb
1
2
3
4
5
6
require 'rails_helper'

feature 'tweet', type: :feature do
# このブロックの内部にscenarioを記述していく

end

Scenarioを作成する。今回は、ユーザーが実際にログインして、ツイートを投稿できるかどうかをフィーチャスペックで確かめる。

まずはルートにアクセスして、期待するHTML要素が描画されているかどうか確かめる。

ルートにアクセスする処理を再現する

spec/features/tweet_spec.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
require 'rails_helper'

feature 'tweet', type: :feature do
  let(:user) { create(:user) }

  scenario 'post tweet' do
    # ログイン前には投稿ボタンがない
    visit root_path
    expect(page).to have_no_content('投稿する')
  end
end

visit root_pathでルートを開くことができる。
have_no_contentマッチャは、引数に指定したバリューを持つHTML要素がそのページに存在しないことを確かめるためのマッチャ。pictweetは、ログインしなければ投稿ボタンが現れない仕様になっているため、ここでは投稿ボタンが存在しないことを確かめている。

ログイン処理を再現する

ブラウザ上でログインする流れを整理すると、次のような4ステップになる。

(1)ログインフォームのあるページに移動する
(2)メールアドレスを入力する
(3)パスワードを入力する
(4)ログインボタンを押す

spec/features/tweet_spec.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
require 'rails_helper'

feature 'tweet', type: :feature do
  let(:user) { create(:user) }

  scenario 'post tweet' do
    # ~省略~

    # ログイン処理
    visit new_user_session_path
    fill_in 'user_email', with: user.email
    fill_in 'user_password', with: user.password
    find('input[name="commit"]').click
    expect(current_path).to eq root_path
    expect(page).to have_content('投稿する')
  end
end

deviseを導入することによって生成されるログインページのプレフィックスは、new_user_session_path
fill_inメソッドを使って値が入力される動きを作成する。

idがuser_emailのフォームにはlet(:user)で作成したuserのemail、idがuser_passwordのフォームにはuserのpasswordを入力している。

フォームに必要な情報を入力した後は、find('input[name="commit"]').clickでログインボタンをクリックしている。

ログイン処理後はルートにリダイレクトされるようになっている。expect(current_path).to eq root_pathで、ルートに移動したことを確かめ、expect(page).to have_content('投稿する')で、ログイン状態では投稿ボタンが表示されることを確認している。

ログイン状態が再現できたので、最後にツイートを作成して投稿できるかをテストする。

spec/features/tweet_spec.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
require 'rails_helper'

feature 'tweet', type: :feature do
  let(:user) { create(:user) }

  scenario 'post tweet' do
    # ログイン前には投稿ボタンがない
    visit root_path
    expect(page).to have_no_content('投稿する')

    # ログイン処理
    visit new_user_session_path
    fill_in 'user_email', with: user.email
    fill_in 'user_password', with: user.password
    find('input[name="commit"]').click
    expect(current_path).to eq root_path
    expect(page).to have_content('投稿する')

    # ツイートの投稿
    expect {
      click_link('投稿する')
      expect(current_path).to eq new_tweet_path
      fill_in 'image', with: 'https://s.eximg.jp/expub/feed/Papimami/2016/Papimami_83279/Papimami_83279_1.png'
      fill_in 'text', with: 'フィーチャスペックのテスト'
      find('input[type="submit"]').click
    }.to change(Tweet, :count).by(1)
  end
end