私がインテグレーションテストを書く際のやり方について

前回、私がRSpecを書く場合のやり方についての記事を書きました。
今回はインテグレーションテストについて書きます。

私の場合はController,Viewのテストは基本的には書かず、インテグレーションテストで済ませています。インテグレーションテストフレームワークは、

  • Cucumber
  • Turnip
  • RequestSpec
  • FeatureSpec
  • Steak

など色々あります。以前はCucumberを使っていたのですが、現在担当しているプロジェクトを始める前に、Cucumberに不満があったので、他のやつを試そうという話になって、Steak, RequestSpec, FeatureSpecを試して、FeatureSpecにしました。Turnipはこれらを検討していた頃は有名でなかったので、試していませんが、うちの会社の他のプロジェクトでは、Cucumberの代わりにTurnipを使うようになっています。

ちなみになんでCucumberをやめたかというと、Cucumberは日本語が書けるけれど、別にプログラマがテストできていればいいということであれば、日本語でテスト内容を書くのは二度手間ではないか?という点があったからです。仕様についてはit(scenario)に日本語で書けるし、ブラウザの操作部分を日本語で書く旨味ってなんだろうか?ということになったときに、『自分たちだけが確認すればいい状況だったらメリットはそこまでないよね』ということで、私の担当しているプロジェクトではFeatureSpecを使ってます。

私がインテグレーションテストを行う場合の環境について

私が書く場合の話なので、ここでもFeatureSpecについて書きます。

ちなみにFeatureSpecは以下のものを使って環境を作ってます。

  • RSpec
  • FactoryGirl
  • Capybara(ヘッドレスブラウザ)
  • PhantomJS(ヘッドレスなJavaScriptが使える環境)
  • Poltergeist(仮想ブラウザでJavaScriptを操作)
  • DatabaseRewinder(データベースの掃除を行う)

Seleniumなど、実際のブラウザを使うようなテストにすると速度が遅くなりすぎるので、ヘッドレスなブラウザを使います。これらを使うためのspec_helper.rbの設定の書き方は、ここでは省略します。リクエストがあったら公開を考えます。

FeatureSpecはRSpecなので、書き方は基本的に普通のRSpecと同じですが、インテグレーションテスト用の書き方が準備されています。

feature
describeと同じ
background
beforeと同じ
scenario
itと同じ

インテグレーションテストの場合はシナリオ単位でテストするから、scenarioというDSLはしっくりきます。

テストをするときに意識するポイント

インテグレーションテストは人間の手でやると無茶苦茶時間がかかるところなので、私はできるだけ多くのパターンを書いておきます。が、モデルのテストと重複するようなものは省略します。実際にユーザーがやりそうな操作を想像して、正常系から徐々に異常系のテストも足していってテストを育てていきましょう。

スローテストになりやすいので注意

ただし、インテグレーションテストはブラウザを経由するため、動作はやはり遅くなります。数秒から十数秒で終わるModelのテストに比べると、かなり違います。データの投入はなるべく最小限、かつ、JavaScriptを使うところと使わないところの見極めが大事になります。JavaScriptを必要としないページの場合はなるべくJavaScriptを使う設定をOnにしないようにします。JavaScriptが使えるようにすると、途端にテストのスピードが落ちるからです。

ログイン処理のテストシナリオを書いてみる

では、ログインを例としてテストを書いてみましょう。

feature 'ログイン処理' do
  background do
    visit new_user_session_path
  end
  context '登録済みユーザーでログインを試みた場合' do
    scenario 'ログインできること'
  end
  context '間違ったパスワードを入力した場合' do
    scenario 'エラーメッセージが表示されること'
  end
end

とりあえず思いついたテストは2つとします。

成功のパターン

まず、ログインできること、のテストを記述します。

backgroundブロックで、ログイン用のユーザーを作っておきました。

context '登録済みユーザーでログインを試みた場合' do
  background do
    FactoryGirl.create :user
  end
  scenario 'ログインできること' do
    fill_in 'user_email',    with: 'patorash@email.com'
    fill_in 'user_password', with: 'password'
    click_button 'ログイン'
    expect(page).to have_content 'ログインしました'
  end
  # 略
end

ログインに成功したときにだけある文章を見つけたら、ログインに成功したこととします。

失敗のパターン

次に、ログインに失敗したときのパターンです。
今度はbackgroundブロックを作っていません。
これはユーザーを保存するとデータベースへのアクセスが発生してテストが遅くなるからです。なるべくDBにデータを入れないように注意しておくと、スローテストになりにくいと思います。

context '間違ったパスワードを入力した場合' do
  scenario 'ログインできないこと'
    fill_in 'user_email',    with: 'patorash@email.com'
    fill_in 'user_password', with: 'password'
    click_button 'ログイン'
    expect(page).to have_no_content 'ログインしました'
    expect(page).to have_content 'メールアドレスかパスワードが違います'
  end
end

ログインしましたというメッセージがないことと、エラーメッセージがでていることを確認しています。

ログイン後の機能をテストする

ログイン後の機能をテストしてみたいと思います。
まずはすごくベタに書いてみます。テストする内容は「ブログの記事一覧を表示」とかにしてみましょう。

feature 'ブログの記事について' do
  background do
    FactoryGirl.create :user
    visit new_user_session_path
    fill_in 'user_email', with: 'patorash@email.com'
    fill_in 'user_password', with: 'password'
    click_button 'ログイン'
    visit articles_path
  end
  context '記事が既に存在する場合' do
    scenario '記事のタイトル一覧が表示されること'
  end
  context '記事が無い場合' do
    scenario 'まだ記事がありませんと表示されること'
  end
end

シナリオはこんな感じだと思います。このシナリオのテストは、適当に書いてもらうとしましょう。ここで何に言及したいかというと、backgroundブロックに書いたログイン処理です。この処理、さっきのテストでも書いていますね?こういうブラウザ上での処理を毎回書いていると、とても大変です。なので、関数にしてしまいましょう。

ログイン処理をテストのスコープ内で関数化する

feature 'ブログの記事について' do

  def create_user_and_login
    FactoryGirl.create :user
    visit new_user_session_path
    fill_in 'user_email', with: 'patorash@email.com'
    fill_in 'user_password', with: 'password'
    click_button 'ログイン'
  end

  background do
    create_user_and_login
    visit articles_path
  end
  # 略
end

関数になったことで、背景でなにをしているのかがわかりやすくなりました。「ユーザー作ってからログインしてるんか、ふむふむ…」という感じですね。このテスト内容のみで使いそうな関数ならば、このfeatureファイル内だけで関数を定義すればいいと思いますが、他のfeatureでも同じような処理を使い回しそうだなと思ったら(とくにこのログイン処理のようなもの)、関数を外部に定義しておきましょう。

feature用の関数を定義する

私の場合、spec/support以下に、テスト用のヘルパーを作っています。用途によっていろいろなヘルパーを作りますが、今回はfeature_helpers.rbを作ってみます。

module FeatureHelpers
  def create_user_and_login
    FactoryGirl.create :user
    visit new_user_session_path
    fill_in 'user_email', with: 'patorash@email.com'
    fill_in 'user_password', with: 'password'
    click_button 'ログイン'
  end
end

このヘルパーはfeatureのテストからだけ呼び出せるように、spec_helperで定義しておきます。

RSpec.configure do |config|
  config.include FeatureHelpers, type: :feature
end

こうすることで、spec/features/ならどこからでもログイン用関数が呼べるようになりました。

feature 'ブログの記事について' do
  background do
    create_user_and_login
    visit articles_path
  end
  # 略
end

Ajaxのテストを行う(JavaScriptを含むテスト)

先ほどのブログの記事のタイトル一覧の表示がAjaxで取得されているとします。ブラウザがJavaScriptを使えない場合、テストが成功しないため、Ajaxのテストを行いたい場合は、テスト内でJavaScriptを有効にします。
JavaScriptを有効にするには、js: trueを設定します。

feature 'ブログの記事について' do
  background do
    create_user_and_login
    visit articles_path
  end
  context '記事が既に存在する場合' do
    scenario '記事のタイトル一覧が表示されること', js: true do
      expect(page).to have_content '初めての投稿です。'
    end
  end
  # 略
end

ちなみにこのjs: trueですが、このテスト内の全てでJavaScriptを使う場合は、以下のようにもできます。

feature 'ブログの記事について', js: true do
  background do
    create_user_and_login
    visit articles_path
  end
  context '記事が既に存在する場合' do
    scenario '記事のタイトル一覧が表示されること' do
      expect(page).to have_content '初めての投稿です。'
    end
  end
  # 略
end

ちなみにdescribe, context毎にも設定可能です。シナリオによってはJavaScriptが要らない場合、できればシナリオ単位で有効にしましょう。JavaScriptをオフにしているテストは高速になりますから、シナリオ単位でJavaScriptを有効にしたほうがテストの速度に違いが出てきます。

入力項目がちょっと違うテストを簡単にする

画面系のテストはなにしろ入力のテストが面倒です。たとえば、ユーザー作成画面で色んなパターンを入力させてみたとしましょう。

feature 'ユーザー管理' do
  background do
    create_user_and_login
    visit users_path
  end
  context 'ユーザー登録で' do
    background do
      click_link '新規作成'
    end
    scenario 'ユーザーを登録できること' do
      fill_in 'user_name', with: 'サンプルユーザー'
      fill_in 'user_email', with: 'sample@email.com'
      fill_in 'user_password', with: 'password'
      fill_in 'user_password_confirmation', with: 'password'
      click_button '登録'
      expect(page).to have_content '登録しました'
    end
    scenario '登録済みのメールアドレスはエラーになること' do
      fill_in 'user_name', with: 'サンプルユーザー'
      fill_in 'user_email', with: 'patorash@email.com' # 登録済みメールアドレス
      fill_in 'user_password', with: 'password'
      fill_in 'user_password_confirmation', with: 'password'
      click_button '登録'
      expect(page).to have_content '既に登録済みです'
    end
    scenario 'パスワードが短すぎるとエラーになること' do
      fill_in 'user_name', with: 'サンプルユーザー'
      fill_in 'user_email', with: 'sample@email.com'
      fill_in 'user_password', with: 'p'
      fill_in 'user_password_confirmation', with: 'p'
      click_button '登録'
      expect(page).to have_content 'パスワードは8文字以上にしてください'
    end
  end
end

同じ事を何度も書いていてすごく…冗長です。でもまぁコピペでできるからよくない?と思うかもしれませんが、入力項目が後で追加されると全部修正していかなくてはなりません。それは面倒です。ですので、これもまとめておきましょう。ブロックを使います。FeatureHelpersに新しいメソッドを追加しましょう。

module FeatureHelpers
  # 略
  def input_and_submit_user_info
      fill_in 'user_name', with: 'サンプルユーザー'
      fill_in 'user_email', with: 'sample@email.com'
      fill_in 'user_password', with: 'password'
      fill_in 'user_password_confirmation', with: 'password'
      yield if block_given?
      click_button '登録'
  end
end

これを使って書き直してみます。

feature 'ユーザー管理' do
  background do
    create_user_and_login
    visit users_path
  end
  context 'ユーザー登録で' do
    background do
      click_link '新規作成'
    end
    scenario 'ユーザーを登録できること' do
      input_and_submit_user_info
      expect(page).to have_content '登録しました'
    end
    scenario '登録済みのメールアドレスはエラーになること' do
      input_and_submit_user_info do
        fill_in 'user_email', with: 'patorash@email.com' # 登録済みメールアドレス
      end
      expect(page).to have_content '既に登録済みです'
    end
    scenario 'パスワードが短すぎるとエラーになること' do
      input_and_submit_user_info do
        fill_in 'user_password', with: 'p'
        fill_in 'user_password_confirmation', with: 'p'
      end
      expect(page).to have_content 'パスワードは8文字以上にしてください'
    end
  end
end

登録ボタンを押す前に、入力項目を上書きするチャンスをブロックで与えています。こうすることで、フォーム入力を一部書き換えたい場合など、柔軟に対応できるようになりました。

テストもわかりやすさが重要!

とりあえず、以上が私がインテグレーションテストをする際に意識している点です。実践的な内容だと他にもあると思いますが、前回と今回で何が言いたかったかというと、『テストもわかりやすく、速く動くように書こう』ということです。何をテストしているのかわかりにくくなってしまうと、テスト自身が負債になってしまう可能性があります。まぁ、私の書き方がわかりにくいんじゃないか?という人もいるとは思います。これが正解でもありませんので、もっといい書き方などがあれば教えてください。


カテゴリー Ruby, Ruby on Rails | タグ | パーマリンク

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です