SystemSpecを書いてみよう

前回の続きで今回はSystemSpecを書いてみます。

SystemSpecとは

システムテストのことをRspecではシステムスペックと言います。
システムテストとは、実際に使用される状況と同じ設定でテストを行い、想定通りに動作するか検証することを言います。

テストの作成

spec/system/user_spec.rbを作成し、ここにユーザーに関するテストを書いていきます。

require 'rails_helper'

RSpec.describe "Users", type: :system do

end

大きく分けて
・ページ遷移
・ユーザー新規作成
・ユーザー編集
・ユーザー削除
の4つに関するテストを作ります。
describeに対象、contextに条件、itに具体的なテスト内容を記述します。

require 'rails_helper'

RSpec.describe "Users", type: :system do
  describe 'ページ遷移確認' do
  end

  describe 'ユーザー新規作成' do
  end

  describe 'ユーザー編集' do
  end

  describe 'ユーザー削除' do
  end
end

まずは対象で分けてみました。
次はdescribeの中にcontextで条件を書いていきます。

require 'rails_helper'

RSpec.describe "Users", type: :system do
  describe 'ページ遷移確認' do
    context 'ユーザーの一覧ページに遷移' do

    end

    context 'ユーザーの新規作成ページに遷移' do

    end

    context 'ユーザーの編集ページに遷移' do

    end

    context 'ユーザーの詳細ページに遷移' do

    end
  end


  describe 'ユーザー新規作成' do
    context 'フォームの入力値が正常な場合' do

    end

    context '名前が未入力の場合' do

    end
  end

  describe 'ユーザー編集' do
    context 'フォームの入力値が正常な場合' do

    end

    context '名前が未入力の場合' do

    end
  end

  describe 'ユーザー削除' do

  end
end

こんな感じになりました。
テストは正常系異常系を作成します。
正常系は、全てのフォームが入力されている場合など、想定通りの操作をしたときの挙動を確認します。
異常系は、必須項目のフォームが空の場合など、想定外の操作をしたときの挙動を確認します。
なおユーザー削除は必ず成功する想定なので正常系のみです。

ページ遷移確認のテスト

RSpec.describe "Users", type: :system do
let(:user) { create(:user) }

  describe 'ページ遷移確認' do
    context 'ユーザーの一覧ページに遷移' do
      it 'ユーザーの一覧ページへのアクセスに成功する' do
        user_list = create_list(:user, 3)
        visit users_path
        expect(page).to have_content 'ユーザー一覧'
        expect(page).to have_content user_list[0].name
        expect(page).to have_content user_list[1].name
        expect(page).to have_content user_list[2].name
        expect(current_path).to eq users_path
      end
    end

    context 'ユーザーの新規作成ページに遷移' do
      it 'ユーザーの新規作成ページへのアクセスに成功する' do
        visit new_user_path
        expect(page).to have_content 'ユーザー新規作成'
        expect(current_path).to eq new_user_path
      end
    end

    context 'ユーザーの編集ページに遷移' do
      it 'ユーザーの編集ページへのアクセスに成功する' do
        visit edit_user_path(user)
        expect(page).to have_content 'ユーザー編集'
        expect(page).to have_field 'Name', with: user.name
        expect(page).to have_field 'Gender', with: user.gender
        expect(current_path).to eq edit_user_path(user)
      end
    end

    context 'ユーザーの詳細ページに遷移' do
      it 'ユーザーの詳細ページへのアクセスに成功する' do
        visit user_path(user)
        expect(page).to have_content 'ユーザー詳細'
        expect(page).to have_content user.name
        expect(page).to have_content user.gender
        expect(current_path).to eq user_path(user)
      end
    end
  end
end

例として2つ目のテストを挙げると
visit new_user_pathユーザー新規作成ページを訪れ
expect(page).to have_content 'ユーザー新規作成'そのページに「ユーザー新規作成」と表示されていて
expect(current_path).to eq new_user_path今いるpathがnew_user_path
ならテストが通ります。

let(:user) { create(:user) }は、itの中でその都度user = create(:user)でuserを作成するのではなく、予め定義だけしておいてuserで呼び出せるようにしています。

残りのテスト

全部説明すると長くなってしまうので残り一気にいっちゃいます。
最終的なコードがこちら。

require 'rails_helper'

RSpec.describe "Users", type: :system do
let(:user) { create(:user) }

  describe 'ページ遷移確認' do
    context 'ユーザーの一覧ページに遷移' do
      it 'ユーザーの一覧ページへのアクセスに成功する' do
        user_list = create_list(:user, 3)
        visit users_path
        expect(page).to have_content 'ユーザー一覧'
        expect(page).to have_content user_list[0].name
        expect(page).to have_content user_list[1].name
        expect(page).to have_content user_list[2].name
        expect(current_path).to eq users_path
      end
    end

    context 'ユーザーの新規作成ページに遷移' do
      it 'ユーザーの新規作成ページへのアクセスに成功する' do
        visit new_user_path
        expect(page).to have_content 'ユーザー新規作成'
        expect(current_path).to eq new_user_path
      end
    end

    context 'ユーザーの編集ページに遷移' do
      it 'ユーザーの編集ページへのアクセスに成功する' do
        visit edit_user_path(user)
        expect(page).to have_content 'ユーザー編集'
        expect(page).to have_field 'Name', with: user.name
        expect(page).to have_field 'Gender', with: user.gender
        expect(current_path).to eq edit_user_path(user)
      end
    end

    context 'ユーザーの詳細ページに遷移' do
      it 'ユーザーの詳細ページへのアクセスに成功する' do
        visit user_path(user)
        expect(page).to have_content 'ユーザー詳細'
        expect(page).to have_content user.name
        expect(page).to have_content user.gender
        expect(current_path).to eq user_path(user)
      end
    end
  end


  describe 'ユーザー新規作成' do
    before { visit new_user_path }

    context 'フォームの入力値が正常な場合' do
      it 'ユーザーの新規作成が成功する' do
        fill_in 'Name', with: 'test_name'
        select '男性', from: 'Gender'
        click_button '登録する'
        expect(page).to have_content 'User was successfully created.'
        expect(current_path).to eq '/users/1'
      end
    end

    context '名前が未入力の場合' do
      it 'ユーザーの新規作成が失敗する' do
        fill_in 'Name', with: nil
        select '男性', from: 'Gender'
        click_button '登録する'
        expect(page).to have_content '1 error prohibited this user from being saved:'
        expect(page).to have_content 'Nameを入力してください'
        expect(current_path).to eq users_path
      end
    end
  end

  describe 'ユーザー編集' do
    before { visit edit_user_path(user) }

    context 'フォームの入力値が正常な場合' do
      it 'ユーザーの編集が成功する' do
        fill_in 'Name', with: 'update_name'
        select '男性', from: 'Gender'
        click_button '更新する'
        expect(page).to have_content 'User was successfully updated.'
        expect(current_path).to eq '/users/1'
      end
    end

    context '名前が未入力の場合' do
      it 'ユーザーの編集が失敗する' do
        fill_in 'Name', with: nil
        select '男性', from: 'Gender'
        click_button '更新する'
        expect(page).to have_content '1 error prohibited this user from being saved:'
        expect(page).to have_content 'Nameを入力してください'
        expect(current_path).to eq user_path(user)
      end
    end
  end

  describe 'ユーザー削除' do
    let!(:user) { create(:user) }

    it 'ユーザーの削除が成功する' do
      visit users_path
      click_link 'Destroy'
      expect(page.accept_confirm).to eq 'Are you sure?'
      expect(page).to have_content 'User was successfully destroyed.'
      expect(current_path).to eq users_path
    end
  end
end

・ユーザー新規作成のbefore { visit new_user_path }
describe 'ユーザー新規作成'内の2つのcontextは両方ともまずnew_user_pathにアクセスするところから始まります。それぞれのテストにいちいち書くのはDRYに反しますね。なのでbeforeでまとめてしまいましょう。ユーザー編集のbeforeも同様です。
・ユーザー削除のlet!(:user) { create(:user) }
先ほどのletと違い、!がついています。
2つの違いはuserが作成されるタイミングです。

・letはuserが呼び出されたときに作成され、
・let!はテスト実行前に作成されます。

はい、よくわかりません。
具体的に見てみます。ユーザー編集の正常系のテストはこのようになっています。

RSpec.describe "Users", type: :system do
let(:user) { create(:user) }

略

  describe 'ユーザー編集' do
    before { visit edit_user_path(user) }

    context 'フォームの入力値が正常な場合' do
      it 'ユーザーの編集が成功する' do
        fill_in 'Name', with: 'update_name'
        select '男性', from: 'Gender'
        click_button '更新する'
        expect(page).to have_content 'User was successfully updated.'
        expect(current_path).to eq '/users/1'
      end
    end

該当のユーザーの編集ページを訪れ
Nameに「update_name」と入力し
Genderは「男性」を選択
「更新する」ボタンをクリックすると
ページに「User was successfully updated.」と表示されていて
そのページのpathは「/users/1」になっている
とテストが成功する。

という内容です。
この最初のvisit edit_user_path(user)、ここでuserが出てきてます。このタイミングで実際にcreate(:user)されています。

次にユーザー削除のテストを見てみましょう。

  describe 'ユーザー削除' do
    let!(:user) { create(:user) }

    it 'ユーザーの削除が成功する' do
      visit users_path
      click_link 'Destroy'
      expect(page.accept_confirm).to eq 'Are you sure?'
      expect(page).to have_content 'User was successfully destroyed.'
      expect(current_path).to eq users_path
    end
  end

こちらはlet!(:user) { create(:user) }としてあります。
このテストは

users_path(ユーザー一覧ページ)を訪れ
「Destroy」というリンクをクリック
アラートで「Are you sure?」と表示され
OKすると「User was successfully destroyed.」と表示され
users_pathに遷移される

とテスト成功です。
が、itの中を見てみるとuserはどこにも呼び出されていません。
しかしそもそもユーザーが1件もないとDestroyというリンクは表示されないような実装にしているので、itの中でuserを作成する必要があります。
let(:user) { createa(:user) }と定義しているだけではuserは作成されないので、let!(:user) { create(:user) }でテスト実行前に予め作成しておかないといけません。

ユーザー削除のテストの結果をletとlet!で見比べてみます。
letの場合

Image from Gyazo

let!だと想定通り、テストが通りますが、
letの場合

Image from Gyazo

「Destroy」なんて見つからない、と言われてしまいます。
前述の通り、ユーザーが0件だと「Destroy」が表示されない、つまりユーザーが作成されていない。
削除のテストを実行するためにはやはりletではなくlet!でユーザーの作成が必要だとわかります。

最後に

簡単なCRUDアプリなのでシンプルなテストになりました。もっと多機能なアプリだとテストも複雑になりそうです。あとテストではハードコーディングしたほうがいいのかなー、と思ったんですがどうなんでしょう。テスト件数が増えるとメンテナンスが大変になりそうですがそっちの方が堅牢なテストになりそうな気もします。
今回も自分にとってしっくり来る表現をしたので厳密には間違ってたりするかもしれません。ご指摘あればコメントいただけると嬉しいです。
長くなりましたが、読んでいただきありがとうございました。

参考サイト

使えるRSpec入門・その4「どんなブラウザ操作も自由自在!逆引きCapybara大辞典」 - Qiita
Module: Capybara::Node::Actions — Documentation for jnicklas/capybara (master)
システムテストとは?開発段階のテストの流れと主な種類|発注成功のための知識が身に付く【発注ラウンジ】<
RSpecの(describe/context/example/it)の使い分け - Qiita
【直感的に書ける!】RspecでRubyのテストを覚えよう! | プロぽこ
RSpec の letとlet!とbeforeの挙動と実行される順番 - Qiita