RSpecテスト②コード解説(FactoryBot,Faker,let,Capybara)
前回
今回はコードの中身を解説していきます。
RSpecの構成・コード解説
2024年12月04日Qiitaでまとめたこと
RSpecテストとは
Railsでよくつかわれるテストツール。
「人間が読んでも分かりやすいコードでテストを書くこと」を目的としている。
Gemをインストールして、簡単にテストできる。
describe
これから○○のテストを始めるよと、テストのタイトル的な感じ。
describe 'ログインのテスト' do
end
describe '新規登録のテスト' do
end
describe '商品管理機能のテスト' do
end
context
describeをパターン分けをして、状況ごとにテストの流れを整理する。
ログインのテストなら、成功・失敗などパターンが発生するので、下記のように分岐させる。
describe 'ログインのテスト' do
context 'ログイン成功した場合のテスト' do
end
context 'ログイン失敗した場合のテスト' do
end
end
describe '商品管理機能のテスト' do
context '商品が正しく登録される場合のテスト' do
end
context '商品登録が失敗する場合のテスト' do
end
end
it
パターン分けした後に、どのような状態になってることを確認したいのか、具体的に。
describe 'ログインのテスト' do
context 'ログイン成功した場合のテスト' do
it 'ログイン後のリダイレクト先が正しいか' do
end
end
context 'ログイン失敗した場合のテスト' do
it 'ログイン失敗後のリダイレクト先が正しいか' do
end
end
end
describe '商品管理機能のテスト' do
context '商品が正しく登録される場合のテスト' do
it '保存された商品の情報が正しいか' do
end
end
context '商品登録が失敗する場合のテスト' do
it 'エラーメッセージが正しいか' do
end
end
end
expect( ).to と eq
expect( ).to
は、( )内を期待値とする。
eq
は、その期待とeqの後に続く値が同じかを確認。
expect( ).to
を エクスペクテーション
、
eq
を マッチャー
と呼ぶ。
下記は、「ログイン後、example.com/users/1にリダイレクトする」ようチェックしてる
current_path
:
現在のページのパスを取得する。
.to eq
:
その後の値と等しいか確認。この場合「current_path」と等しいか。
'/users/' + user.id.to_s
:
ユーザIDをto_sで文字列にし、それをusersと結合する。
例えば、user.id が 1 の場合、「/users/1」とする。
describe 'ログインのテスト' do
context 'ログイン成功した場合のテスト' do
it 'ログイン後のリダイレクト先が正しいか' do
expect(current_path).to eq '/users/' + user.id.to_s
end
end
end
describe '商品管理機能のテスト' do
context '商品が正しく登録される場合のテスト' do
it '保存された商品の情報が正しいか' do
product = Product.create(name: 'サンプル商品', price: 1000)
# 商品の名前と価格が正しいかを確認
expect(product.name).to eq('サンプル商品')
expect(product.price).to eq(1000)
end
end
context '商品登録が失敗する場合のテスト' do
it 'エラーメッセージが正しいか' do
# 商品名が空の場合
product = Product.create(name: '', price: 1000)
# 商品名が空だとエラーメッセージが表示されることを確認
expect(product.errors[:name]).to eq(["can't be blank"])
end
end
end
他にもよく使われるマッチャー:
-
be
:
オブジェクトが同一であるかを確認(例:expect(obj1).to be obj2
)。 -
include
:
配列やハッシュに特定の要素が含まれているかを確認(例:expect(array).to include value
)。 -
match
:
正規表現に一致するかを確認(例:expect(string).to match /pattern/
)。
before
「フック」と呼ぶ。
個々のテストを実行する前に、まとめて共通する部分の前処理をする。
例えば、以下のようなログインのテストの場合、ログインするためのメールアドレスや名前が必要になる。
beforeを使うと、テストの度毎回書く必要がなく、まとめて書くことができる。
-
fill_in 'user[name]', with: user.name
- 「名前」入力欄に、
user.name
(テスト用のユーザーの名前)を入力させる。
- 「名前」入力欄に、
-
fill_in 'user[password]', with: user.password
- 「パスワード」入力欄に、
user.password
(テスト用のユーザーのパスワード)を入力させる。
- 「パスワード」入力欄に、
-
click_button 'Log in'
- 「ログイン」ボタンをクリックさせる。
beforeを使わないと…
# beforeを使わなかった場合
describe 'ログインのテスト' do
context 'ログイン成功した場合のテスト' do
it 'ログイン後のリダイレクト先が正しいか' do
# ログイン処理を毎回書く必要がある
fill_in 'user[name]', with: user.name
fill_in 'user[password]', with: user.password
click_button 'Log in'
expect(current_path).to eq '/users/' + user.id.to_s
end
it 'ログイン後にユーザー名が表示されるか' do
# ここでも同じ処理を繰り返し書かないといけない
fill_in 'user[name]', with: user.name
fill_in 'user[password]', with: user.password
click_button 'Log in'
expect(page).to have_content user.name
end
end
end
beforeを使うと…
# beforeを使った場合
describe 'ログインのテスト' do
context 'ログイン成功した場合のテスト' do
# ここにまとめて書ける!コードが簡潔!
before do
fill_in 'user[name]', with: user.name
fill_in 'user[password]', with: user.password
click_button 'Log in'
end
it 'ログイン後のリダイレクト先が正しいか' do
expect(current_path).to eq '/users/' + user.id.to_s
end
it 'ログイン後にユーザー名が表示されるか' do
expect(page).to have_content user.name
end
end
end
describe 'テストのタイトル(これから○○のテストを始める)' do
context 'describeを状況ごとにパターン分け(ログインなら成功・失敗など)' do
it 'パターン分け後に、どのような状態になってることを確認したいのか' do
# ○○が★★になってるかを確認
expect(○○).to eq('★★')
# expect(実際の値).to eq('期待値') = 実際の値が期待値になってるか確認
# expect( ).to を エクスペクテーション、
# eq を マッチャーと呼ぶ。
end
end
end
FactoryBot
FactoryBotは、「テストデータを簡単に作成するためのライブラリ」。
テストデータ作成をサポートするためのGem。 Gemfileに以下を記載する。group :test do
gem "factory_bot_rails"
end
FactoryBotを使用するには、「spec/factories/モデル名.rb」のファイルを作成する。
通常、データベースを使ったテストを行う場合、モデルのインスタンスを作成する必要があるが、FactoryBotを使うと、繰り返し同じようなデータを簡単に生成できる。
FactoryBot.define do
factory :task do
title { Faker::Lorem.characters(number:10) }
keyword1 { Faker::Lorem.characters(number:10) }
keyword2 { Faker::Lorem.characters(number:10) }
keyword3 { Faker::Lorem.characters(number:10) }
end
end
Faker
ランダムなテストデータ(名前、住所、メールアドレス、文章など)を生成するためのRubyのライブラリ。テストとかシードデータ(db/seeds.rb)を作るときに便利。
テスト用のダミーデータを作成するためのGem。 Gemfileに以下を書くことで使用可能。
group :test do
gem 'faker'
end
FactoryBot.define do
factory :task do
title { Faker::Lorem.characters(number:10) }
keyword1 { Faker::Lorem.characters(number:10) }
keyword2 { Faker::Lorem.characters(number:10) }
keyword3 { Faker::Lorem.characters(number:10) }
end
end
Lorem
lorem ipsumの略。(ダミーテキスト(仮の文章)の英語表現)
ランダムな文章や単語を生成するための Faker のモジュール。
Loremの後にダミーテキストの種類を書く。
Fakerは、ランダムな文字列だけでなく、下記のように名前やメアドのダミーデータも作成できる。
Faker::Name.name # => "John Doe"
Faker::Internet.email # => "mike.ross@example.com"
モデルファイルのテスト
- バリデーションやメソッドの動作をチェック。
- specフォルダ内に「modelフォルダ」を作成する。
- 「モデル名+_spec.rb」というファイルを作成する。
- ファイルの先頭に下記を記入。
設定などを行うファイル(spec/rails_helper.rb)を読み込んでいる。
require 'rails_helper'
コントローラファイルのテスト
- リクエストを送ったときのレスポンスをチェックする。
- specフォルダ内に「systemフォルダ」を作成する。
- モデルファイルのテストと同様に「モデル名+_spec.rb」というファイルを作成する。
let
let(遅延評価)
- let(:user) は it の中で初めて使われたときに create(:user) が実行される。
- プログラムが「 it 'ログイン成功後にユーザーページに遷移する' do」のコードを実行された時に初めてletが実行されてるということ
- fill_in 'Email', with: user.email で user を使った時点で user が作成される。
- テストを実行するまでデータが作られないので、不要なデータを減らせる!
describe 'ログインのテスト' do
let(:user) { create(:user, email: 'test@example.com', password: 'password') }
it 'ログイン成功後にユーザーページに遷移する' do
visit login_path
fill_in 'Email', with: user.email
fill_in 'Password', with: 'password'
click_button 'ログイン'
expect(current_path).to eq(user_path(user)) # ユーザーページに遷移することを確認
end
end
let!(事前評価)
- let!(:user) は it が実行される前に create(:user) が実行される。
- user を it の中で使う前にデータが作られているので、最初からDBに user が存在する状態。
- it 内で user を使わなくても、データが作成されるので無駄が増える可能性がある。
describe 'ログインのテスト' do
let!(:user) { create(:user, email: 'test@example.com', password: 'password') }
it 'ログイン成功後にユーザーページに遷移する' do
visit login_path
fill_in 'Email', with: user.email
fill_in 'Password', with: 'password'
click_button 'ログイン'
expect(current_path).to eq(user_path(user)) # ユーザーページに遷移することを確認
end
end
letとbefore
bforeと似ているが、beforeはitはブロックの実行時に都度実行されるので、it内で使用してない変数でも実行されてしまう。⇒不要なデータが毎回作成されるので、テストが遅くなる。
let!とFactoryBot
spec/rails_helper.rbに、下記を追加すればFactoryBotを省略することができる
RSpec.configure do |config|
config.include FactoryBot::Syntax::Methods
end
# ↓これを
let!(:book) { FactoryBot.create(:book,title:'hoge',body:'body') }
# ↓このように省略できる
let!(:book) { create(:book,title:'hoge',body:'body') }
Capybara
カピバラ…?
結合テスト(E2Eテスト)を自動化するためのツール。
人間が手動で操作するのと同じ流れをテストできる!
例えば、この流れでテストしたい
- ログインページにアクセス
- メールアドレスとパスワードを入力
- ログインボタンをクリック
- ユーザーのマイページに遷移するか確認
📌 Capybaraのコード例(ログインテスト)
require 'rails_helper'
RSpec.describe 'ログイン機能', type: :system do
let!(:user) { create(:user, email: 'test@example.com', password: 'password') }
it '正しくログインできる' do
visit login_path # 1. ログインページにアクセス
fill_in 'Email', with: user.email # 2. メールアドレスを入力
fill_in 'Password', with: 'password' # 3. パスワードを入力
click_button 'ログイン' # 4. ログインボタンを押す
expect(current_path).to eq(user_path(user)) # 5. ユーザーページに遷移しているか確認
end
end
E2E(End-to-End)テストとは?
アプリケーション全体の動作をユーザー目線でテストする方法。
ログイン、データ入力、ボタンのクリック、画面遷移など、実際のユーザーが操作する流れをシミュレートしてテストする。
他のテストの違い
テストの種類 | 何をテストする? | ツール |
---|---|---|
ユニットテスト | 個々のメソッドやクラスの動作をテスト | RSpec(type: :model ) |
結合テスト | 複数の機能を組み合わせた処理をテスト(例: ユーザー登録 → ログイン) | RSpec(type: :request ) |
E2Eテスト | ユーザー目線でアプリ全体の流れをテスト(ボタンクリック、遷移、表示など) | Capybara(type: :system ) |
Capybaraの実行環境(ドライバ)
Capybaraが動作するには、ドライバ(テストを動かす環境)が必要になる。
ドライバ名 | 特徴 |
---|---|
Rack::Test |
Rails標準のテスト機能で、高速に動作。JavaScriptは動かせない。 |
Selenium |
実際のブラウザ(Chromeなど)を起動して動作確認できる。JavaScriptも対応。ただし動作が遅い。 |
以下はRack::Test
のやり方。
まずspec/spec_helper.rbに設定をする。
RSpec.configure do |config|
config.before(:each, type: :system) do
driven_by :rack_test
end
end
rails newしたデフォルトのGemfileは、元々下記状態になっている。
これでテストすると、Seleniumを使用してテストが行われる。
group :test do
# Adds support for Capybara system testing and selenium driver
gem 'capybara', '>= 3.26'
gem 'selenium-webdriver', '>= 4.0.0.rc1'
# Easy installation and use of web drivers to run system tests with browsers
gem 'webdrivers'
end
RSpecでよく使われるマッチャー名・メソッド
マッチャー名 / メソッド | 機能(使い方) |
---|---|
be_valid | モデルが有効であるか(expect(model).to be_valid )。 |
be_invalid | モデルが無効であるか(expect(model).to be_invalid )。 |
include | 配列や文字列に指定の値が含まれているか。 |
find | ページ内の要素を検索する(find('.classname') など)。 |
click | 指定の要素をクリックする。 |
have_content | ページに特定の文字列が存在するか。 |
have_link | 指定の値のリンクが存在するか(expect(page).to have_link('リンク名', href: path) )。 |
eq | 期待値と値を比較して一致するか(expect(x).to eq(y) )。 |
have_selector | HTMLタグやCSSに指定の文字列が存在するか(expect(page).to have_selector('h1', text: 'タイトル') )。 |
have_field | 指定の入力フォームが存在するか(expect(page).to have_field('email') )。 |
find_all | ページ上の指定のHTMLタグを全て取得する(find_all('div') )。 |
match | 正規表現を使って期待値と一致するか確認する(expect(text).to match(/正規表現/) )。 |
have_button | ページ上に指定のボタンが存在するか(expect(page).to have_button('送信') )。 |
click_button | 指定のボタンをクリックする(click_button '送信' )。 |
have_current_path | 現在のパスが期待通りか(expect(current_path).to eq('/home') )。 |
change | ある動作で値が変化するか(expect { action }.to change { Model.count }.by(1) )。 |
redirect_to | リダイレクト先のURLを確認(expect(response).to redirect_to(path) )。 |
have_http_status | HTTPステータスコードを確認(expect(response).to have_http_status(200) )。 |
render_template | 特定のビューが描画されることを確認(expect(response).to render_template(:index) )。 |
not_to be_valid | モデルが無効であることを確認(expect(model).not_to be_valid )。 |
before | テスト実行前に準備処理を実行する。 |
let | 遅延評価される変数を定義する(it 内で呼ばれるまで実行されない)。 |
let! | 事前評価される変数を定義する(it 実行前に評価される)。 |
create | FactoryBot でデータを作成する(DB に保存)。 |
build | FactoryBot でデータを作成するが、DB には保存しない。 |
create_list | FactoryBot で複数のデータを作成する(create_list(:model, 数) )。 |
attributes_for | FactoryBot でハッシュ形式のデータを生成する。 |
参考文献
Discussion