【RSpec】ユーザー管理機能の結合テストコード

ユーザー管理機能の結合テストコードを書いたので、記録していく。

  • ユーザー新規登録
  • ユーザーログイン
  • ユーザー情報の編集

これらの機能をテストした。

RSpecおさらい

はじめに基本的なおさらいとして、テストには単体テスト結合テストがある。
単体テストは、モデルやコントローラー、ビューなど機能ごとにテストしてバリデーションなどを確認する。
結合テストは、ユーザーの操作を再現して一連の流れが正しい挙動になるかどうかを確認する。よって、結合テストコードではユーザーの操作をexampleに落とし込んで整理して、それをコードで書いていくという流れ。(これが意外と難しい)
またテストは、上手くいくとき(正常系)と上手く行かないとき(異常系)のパターンで書いていく。
※今回の記事では長くなりすぎるため、正常系のコードのみ載せている。

System Spec

結合テストコードを記述するための仕組みを、System Spec といい、CapybaraというGemを用いる。CapybaraはRailsに標準装備されているので、結合テストコードを記述するファイルをさっそく作っていく。

% rails g rspec:system users

このときusersと複数形になっていることに注意

ユーザー新規登録

require 'rails_helper'

RSpec.describe 'ユーザー新規登録', type: :system do
  before do
    @user = FactoryBot.build(:user)
  end

  context 'ユーザー新規登録ができるとき' do
    it '正しい情報を入力すればユーザーは新規登録ができてトップページに移動する' do
      # トップページに移動する
      visit root_path
      # トップページに新規登録ページへ遷移するボタンが表示されていることを確認する
      expect(page).to have_content('新規登録')
      # 新規登録ページへ移動する
      visit new_user_registration_path
      # ユーザー情報を入力する
      fill_in 'name', with: @user.name
      fill_in 'email', with: @user.email
      fill_in 'password', with: @user.password
      fill_in 'password-confirmation', with: @user.password_confirmation
      # 新規登録ボタンを押すとユーザーモデルのカウントが1上がることを確認する
      expect { click_on('登録する')}.to change { User.count }.by(1)
      # 保存されるとトップページへ遷移することを確認する
      expect(current_path).to eq root_path
      # 新規登録ページやログインページへ遷移するボタンが表示されていないことを確認する
      expect(page).to have_no_content('新規登録')
      expect(page).to have_no_content('ログイン')
    end
  end
end

今回、FactoryBotで仮ユーザーを作成していて、before_actionでそれを@userに代入している。
ここで使われているページ遷移やクリックと挙動の期待を確認するマッチャをまとめていく。

特定のページへ移動
visit root_path
visit new_user_registration_path

ちなみに、現在いるページはcurrent_pathとなるため、

visit current_path

とするとページの再読み込みとなる。

クリック

click_on('送信')

ここで使っているclick_onが一番簡単で使いやすい。理由は、クリックする要素がボタンタグでもリンクタグでもどちらでもいけるから。一応クリックする要素ごとに専用のclick_buttonとかclick_linkもある。また、

# ボタンタグ
find("クリックしたい要素").click
# a要素で表示されているリンクのみ
find_link("リンクの文字列", href: "URL").click


という使い分けもある。
ちなみに、findはブラウザ上に表示されている要素しか見えないため、hiddenは見つけられない。そこで、visible: false オプションをつけてあげる。

<input type="hidden" name="secret_value" id="secret_value">
find('#secret_value', visible: false).set('fugahoge')

文字を入力する

 <label for="name" class="form-text">ユーザー名</label>
 <input type="text" name="user[name]" class="input-default" id="name" >
fill_in 'ユーザー名', with: 太郎

「ユーザー名というフォームに太郎と入力する」 このとき、ラベルのforの値とinputのidの値が一致していることで、ブラウザ上に表示されている'ユーザー名'をフォームと認識して取得できている。
ラベルが無いときや、forが無いときはidの値を入れることも可。

画面上(ページ内)の文字列を探す

expect(page).to have_content('新規登録')

ページ内に新規登録という文字列を見つけて、新規登録ボタンが存在することを確認できるマッチャ
反対に、存在しないことを確認したいときは

expect(page).to have_no_content('ログアウト')

モデルのレコード数の変化を確認

expect { click_on('登録する')}.to change { User.count }.by(1)

登録するボタンをクリックすると、ユーザーモデルのレコード数が1増える。つまり、DBに登録情報が保存されたということを確認できる。
注意点としては、changeマッチャでモデルのカウントをする場合のみ、expect( )ではなくexpect{ }となること。

ログイン

RSpec.describe 'ログイン', type: :system do
  before do
    @user = FactoryBot.create(:user)
  end

  context 'ログインできるとき' do
    it '保存されているユーザーの情報と合致すればログインできる' do
      # ログインする
      sign_in(@user)
      # 新規登録ページやログインページへ遷移するボタンが表示されていないことを確認する
      expect(page).to have_no_content('新規登録')
      expect(page).to have_no_content('ログイン')
      # ユーザー詳細ページへ移動する
      visit user_path(@user)
      # ユーザー詳細ページにログアウトボタンが表示されていることを確認する
      expect(page).to have_content('ログアウト')
    end
  end
end

ここでは、"ログインする"という操作を別ファイルから持ってきて使っている。
なぜそうしているかというと、他の機能の結合テストを記述していくときに、ログインしていることが前提なため、その度に"ログインする"という同じ記述を最初にしないといけないというのは、めんどくさい以前にDRYではないから。
この"ログインする"という操作は、新たにsupportディレクトリとsign_in_support.rbファイルを生成して、その中にログイン処理を切り出している。

module SignInSupport
  def sign_in(user)
    visit new_user_session_path
    fill_in 'email', with: user.email
    fill_in 'password', with: user.password
    click_on('ログイン')
    expect(current_path).to eq root_path
  end
end

これを使えるようにするための設定
spec/rails _helper.rbにて

# 中略
# コメントアウトを外す↓
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }
# 中略
RSpec.configure do |config|
  config.include SignInSupport  # ← 追記
# 中略

よって

# ログインする
 sign_in(@user)

テストコードではこれだけの記述で"ログインする"ことができる。

ユーザー情報の編集

RSpec.describe 'ユーザー情報編集', type: :system do
  before do
    @user = FactoryBot.create(:user)
  end

 it 'ユーザー情報の編集がうまくいくとき', js: true do
    # ログインする
    sign_in(@user)
    # ユーザー詳細ページヘ移動する
    visit user_path(@user)
    # ユーザー詳細ページにユーザー情報編集ページへ遷移できるユーザー名のボタンがあることを確認する
    expect(page).to have_content(@user.name)
    # ユーザー情報編集ページへ移動する
    visit edit_user_path(@user)
    # 画像選択フォームに画像を添付する
    attach_file('user_image', Rails.root.join('public/images/user_test_image.png'))
    # ユーザー情報を入力する
    fill_in 'text', with: 'テスト入力'
    fill_in 'name', with: 'example'
    fill_in 'email', with: 'example@example.com'
    # パスワード編集用プルダウンが非表示になっていることを確認する
    expect(find('.accordion-show', visible: false)).to_not be_visible
    # パスワード編集用プルダウンを開く
    find('.change-btn').click
    # 新しいパスワードを入力する
    fill_in 'password', with: 'example1'
    fill_in 'password-confirmation', with: 'example1'
    # 保存ボタンを押す
    click_on('保存する')
    # ユーザー詳細へ遷移することを確認する
    expect(current_path).to eq user_path(@user)
    # 編集したテキストが表示されていることを確認する
    expect(page).to have_content('テスト入力')
    # 編集した画像が表示されていることを確認する
    expect(page).to have_selector('img')
  end
end

テスト画像の添付

(前提としてfactories/users.rbでFactoryBot生成後にテスト画像を差し込んでます。)

FactoryBot.define do
  factory :user do
    name                  { Faker::Name.initials }
    email                 { Faker::Internet.free_email }
    password              { '123abc' }
    password_confirmation { password }

    after(:create) do |item|
      item.image.attach(io: File.open('public/images/user_test_image.png'), filename: 'user_test_image.png')
    end
  end
end
attach_file('user_image', Rails.root.join('public/images/user_test_image.png'))

attach_fileはタイプがfileのinput要素でにファイルを添付できるメソッド。例えば、画像投稿のテスト用の画像を添付できる。
第一引数にinput要素のname属性の値、第二引数にアップロードする画像のパスを記述する。 ちなみにさっき、hiddenで隠されている要素を表に引きずり出すのに、visible: falseオプションを書いたが、attach_fileメソッドで使う場合は,

attach_file('user_image', Rails.root.join('public/images/user_test_image.png'), make_visible: true)


それと、Rails.root.joinは、ルートディレクトリと相対パスをjoinによって結合してるというイメージ

Ajaxを使った処理をテストする

これをメモしときたかった。
前回、パスワード無しのユーザー情報編集機能を実装したという記事を書いたが、そのとき、パスワード入力欄は常に表示されていない方が良いと思って、"パスワード変更"ボタンを押すと、フォームが見えるようになるというJavaScriptによるAjax処理を使った実装をしてました。 そしたらテストコード書くとき、これどうするの?ってなったけど、調べてみるとシンプルだった。

 it 'ユーザー情報の編集がうまくいくとき', js: true do

js: trueタグを付けてあげるだけ。 今回はJavaScriptの実装も簡単なものだったため、これでいけた。

終わりに

テストコードは地味だし、作業になりがちだけどめちゃくちゃ大事なことはなんとなくわかるから、なるべく細かく見て書いていこうと思う。長い記述になりがちだからこそ、ここは切り出せるんじゃないか?みたいな、オブジェクト指向を考えながら手を動かすことでより勉強になると思っている。