前回、私が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
登録ボタンを押す前に、入力項目を上書きするチャンスをブロックで与えています。こうすることで、フォーム入力を一部書き換えたい場合など、柔軟に対応できるようになりました。
テストもわかりやすさが重要!
とりあえず、以上が私がインテグレーションテストをする際に意識している点です。実践的な内容だと他にもあると思いますが、前回と今回で何が言いたかったかというと、『テストもわかりやすく、速く動くように書こう』ということです。何をテストしているのかわかりにくくなってしまうと、テスト自身が負債になってしまう可能性があります。まぁ、私の書き方がわかりにくいんじゃないか?という人もいるとは思います。これが正解でもありませんので、もっといい書き方などがあれば教えてください。