記事概要
Ruby on Railsのテストについて、まとめる
RSpec
Capybara
結合テストコードを記述するための仕組みであり、デフォルトでRailsに搭載されている
まとめ(コマンド)
rspecコマンド
specディレクトリ以下に書かれたRSpecのテストコードを実行するコマンド
まとめ(テスト整理のメソッド)
describeメソッド
テストコードのグループ分けを行うメソッド
「どの機能に対してのテストを行うか」をdescribeでグループ分けし、その中に各テストコードを記述する
contextメソッド
テストコードのグループ分けを行うメソッド
使用方法はdescribeと同じだが、contextには特定の条件を指定する
itメソッド
テストコードのグループ分けを行うメソッド
「describeメソッドに記述した機能において、どのような状況のテストを行うか」を明記する
example
itで分けたグループのこと
まとめ(メソッド)
expectation
検証で得られた挙動が想定通りなのかを確認する構文のこと
雛形は、expect().to matcher()
expectの引数
検証で得られた実際の挙動を指定する
matcher
「expectの引数」と「想定した挙動」が一致しているかどうかを判断する
どのような挙動を想定しているかを記述する
include
「expectの引数」に「includeの引数」が含まれていることを確認するマッチャ
# 配列の中に'メロン'が含まれていることを想定
expect(['りんご', 'バナナ', 'ぶどう', 'メロン']).to include('メロン')
eq
「expectの引数」と「eqの引数」が等しいことを確認するマッチャ
# 1 + 1という計算の結果が、2と等しいことを想定
expect(1 + 1).to eq(2)
be_valid
valid?メソッドの返り値が、trueであることを期待するマッチャ
expectの引数に指定されたインスタンスがバリデーションでエラーにならない場合、valid?の返り値はtrueとなる
@user = FactoryBot.build(:user)
expect(@user).to be_valid
#=> 「@user.valid?」の結果が"true"の場合、正常完了
#=> 「@user.valid?」の結果が"false"の場合、エラー発生
have_content
expect(page).to have_content('X')と記述することで、pageの中に、Xという文字列があるかどうかを判断するマッチャ
# 新規登録ボタンがあることを確認する
expect(page).to have_content('新規登録')
have_no_content
have_contentの逆で、文字列が存在しないことを確かめるマッチャ
change
モデルのレコードの数がいくつ変動するのかを確認できるマッチャ
expect{ [動作] }.to change { [モデル名].count }.by([変動する数])
# サインアップボタンを押すとユーザーモデルのカウントが1上がることを確認する
expect{
find('input[name="commit"]').click
sleep 1
}.to change { User.count }.by(1)
※expect()ではなくexpect{}
have_current_path
pageのURLを確認するマッチャ
# トップページへ遷移することを確認する
expect(page).to have_current_path(root_path)
have_selector
指定したセレクタが存在するかどうかを判断するマッチャ
# 要素「<div class="content_post" style="background-image: url(画像のURL);">」を取得
have_selector ".content_post[style='background-image: url(#{@tweet_image});']"
# トップページには先ほど投稿した内容のツイートが存在することを確認する(画像)
expect(page).to have_selector ".content_post[style='background-image: url(#{@tweet_image});']"
have_link
要素の中に当てはまるリンクがあることを確認するマッチャ
a要素に対して使用する
# 要素の中に当てはまるリンクがあることを確認できる
expect('[要素]').to have_link '[ボタンの文字列]', href: '[リンク先のパス]'
have_no_link
have_linkの逆で、要素の中に当てはまるリンクがないことを確認するマッチャ
# 要素の中に当てはまるリンクがないことを確認できる
expect('[要素]').to have_no_link '[ボタンの文字列]', href: '[リンク先のパス]'
have_field
inputやtextareaなどのフォーム要素が存在するかを確認するマッチャ
引数には、CSSで使用している#などは不要
# 「tweet_image」というidを持ったフォームが存在するかを確認
have_field('tweet_image')
# 「tweet_image」というidを持ったフォームが存在し、そのフォームにHelloと入力されていることを確認
have_field('tweet_image', with: "Hello")
build
ActiveRecordのnewメソッドと同様の意味を持つ
テスト用DBへアクセスしないため、データが保存されない
# FactoryBotを利用しない場合
user = User.new(nickname: 'test', email: 'test@example', password: '000000', password_confirmation: '00000000')
# FactoryBotを利用する場合
user = FactoryBot.build(:user)
create
ActiveRecordのcreateメソッドと同様の意味を持つ
buildとほぼ同じ働きをするが、createの場合はテスト用DBに値が保存される
注意すべき点は、1回のテストが実行され、終了する毎にテスト用DBの内容がロールバックされる(テスト実行時に保存された値がすべて消去されてしまう)
# FactoryBotを利用しない場合
user = User.create(nickname: 'test', email: 'test@example', password: '000000', password_confirmation: '00000000')
# FactoryBotを利用する場合
user = FactoryBot.create(:user)
before
それぞれのテストコードを実行する前に、セットアップを行うこと
require 'rails_helper'
RSpec.describe モデル名, type: :model do
before do
# 処理(変数を受け渡す場合、インスタンス変数にする必要がある)
end
describe 'X' do
it 'Y' do
# before内の処理が完了してから実行される
end
it 'Z' do
# before内の処理が完了してから実行される
end
end
end
after
任意の処理後に指定の処理を実行する
Rails.root
Railsアプリケーションのトップ階層のディレクトリまでの絶対パスを取得できる
# /Users/ユーザー名/projects/に「sample-app」というRailsアプリがある場合
[1] pry(main)> Rails.root
=> #<Pathname:/Users/ユーザー名/projects/sample-app>
Rails.root.join
引数として渡した文字列でのパス情報を、Rails.rootのパスの情報につけることができる
# /Users/ユーザー名/projects/に「sample-app」というRailsアプリがある場合
[1] pry(main)> Rails.root.join('public/images/test_image.png')
=> #<Pathname:/Users/ユーザー名/projects/sample-app/public/images/test_image.png>
create_list
FactoryBotの設定ファイルに存在しているレコードを、複数作成したい場合に使用するメソッド
create_listを用いることで、一度に複数のテストデータを生成可能
# hogesテーブルのレコードを3つ作成
FactoryBot.create_list(:hoge, 3)
# hogesテーブルのレコードを3つ作成し、titleカラムの情報を「Hello world」とする
FactoryBot.create_list(:hoge, 3, title: 'Hello world')
# hogesテーブルのレコードを3つ作成し、外部キー(user_id)の情報を@user.idとする
FactoryBot.create_list(:hoge, 3, user_id: @user.id)
sleep
次の処理を待機する
# 1秒待機後、次の処理を行う
sleep 1
まとめ(単体テストのメソッド)
valid?メソッド
バリデーションを実行させて、エラーがあるかどうかを判断するメソッド
エラーがない場合はtrueを返す
エラーがある場合はfalseを返し、エラーの内容を示すエラーメッセージを生成する
user.valid?
# Userモデルにおいて、カラムnicknameには入力必須のバリデーションが設けられているケース
[1] pry(main)> user = User.new(nickname: '', email: 'test@example', password: '000000', password_confirmation: '000000')
[2] pry(main)> user.valid?
=> false
errors
インスタンスにエラーを示す情報がある場合、その内容を返すメソッド
# 変数userに新規データを格納
[1] pry(main)> user = User.new(nickname: '', email: 'test@example', password: '000000', password_confirmation: '000000')
=> #<User id: nil, email: "test@example", created_at: nil, updated_at: nil, nickname: "">
# 変数userにバリデーションを実行し、エラーがあるかを判断
[2] pry(main)> user.valid?
User Exists? (0.4ms) SELECT 1 AS one FROM `users` WHERE `users`.`email` = BINARY 'test@example' LIMIT 1
=> false
# エラー情報を表示
[3] pry(main)> user.errors
=> #<ActiveModel::Errors [#<ActiveModel::Error attribute=nickname, type=blank, options={}>]>
full_messages
エラーの内容から、エラーメッセージを配列として取り出すメソッド
# 変数userに新規データを格納
[1] pry(main)> user = User.new(nickname: '', email: 'test@example', password: '000000', password_confirmation: '000000')
=> #<User id: nil, email: "test@example", created_at: nil, updated_at: nil, nickname: "">
# 変数userにバリデーションを実行し、エラーがあるかを判断
[2] pry(main)> user.valid?
User Exists? (0.5ms) SELECT 1 AS one FROM `users` WHERE `users`.`email` = BINARY 'test@example' LIMIT 1
=> false
# エラーメッセージを表示
[3] pry(main)> user.errors.full_messages
=> ["Nickname can't be blank"]
user.valid?
expect(user.errors.full_messages).to include("Nickname can't be blank")
get
「パスにリクエストすると」を行う
get Prefix名_path
# ルートパスにリクエストする
get root_path
# showアクションにリクエストする
get tweet_path(@tweet)
response
リクエストに対するレスポンスそのもの
レスポンスで取得できる情報に、想定する内容が含まれているかを確認することで、テストコードを書ける
[1] pry(#<RSpec::ExampleGroups::TweetsController::GETIndex>)> response
=> #<ActionDispatch::TestResponse:0x0000000117117278
@cache_control={:max_age=>"0", :private=>true, :must_revalidate=>true},
@committed=false,
@cv=
#<MonitorMixin::ConditionVariable:0x00000001171902b8
@cond=#<Thread::ConditionVariable:0x0000000117190290>,
@monitor=#<Monitor:0x0000000117190470>>,
@header=
{"X-Frame-Options"=>"SAMEORIGIN",
"X-XSS-Protection"=>"0",
"X-Content-Type-Options"=>"nosniff",
"X-Download-Options"=>"noopen",
"X-Permitted-Cross-Domain-Policies"=>"none",
"Referrer-Policy"=>"strict-origin-when-cross-origin",
"Link"=>
"</assets/application-9b8e2e0d675fa110d5daab5bb63deb13a118ca78232c286410d1b5291170c08e.css>; rel=preload; as=style; nopush",
"Content-Type"=>"text/html; charset=utf-8",
"ETag"=>"W/\"77f2ac4a81f863be39016e9731728738\"",
"Cache-Control"=>"max-age=0, private, must-revalidate",
"X-Request-Id"=>"f1f6e0b0-b6fc-4f8e-81e1-127901377b68",
"X-Runtime"=>"0.327373",
"Content-Length"=>"3460"},
# 十字キーの↓を入力すると続きを確認でき、qを押下すると終了して次の入力画面に移行できる
HTTPステータスコード
HTTP通信において、どのような処理の結果となったのかを示すもの
| ステータスコード | 内容 |
|---|---|
| 100~ | 処理の継続中 |
| 200~ | 処理の成功 |
| 300~ | リダイレクト |
| 400~ | クライアントのエラー |
| 500~ | サーバーのエラー |
status
response.statusを実行することで、HTTPステータスコードを確認できる
# レスポンスのステータスコードが出力される
[1] pry(#<RSpec::ExampleGroups::TweetsController::GETIndex>)> response.status
=> 200
expect(response.status).to eq 200
body
response.bodyを実行することで、ブラウザに表示されるHTMLの情報を抜き出せる
# ブラウザに表示されるHTMLの情報が出力される
[1] pry(#<RSpec::ExampleGroups::TweetsController::GETIndex>)> response.body
=> "<!DOCTYPE html>\n<html>\n <head>\n <title>Pictweet</title>\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n \n \n\n <link rel=\"stylesheet\" href=\"/assets/application-9b8e2e0d675fa110d5daab5bb63deb13a118ca78232c286410d1b5291170c08e.css\" data-turbo-track=\"reload\" />\n <script type=\"importmap\" data-turbo-track=\"reload\">{\n \"imports\": {\n \"application\": \"/assets/application-37f365cbecf1fa2810a8303f4b6571676fa1f9c56c248528bc14ddb857531b95.js\",\n \"@hotwired/turbo-rails\": \"/assets/turbo.min-f3765a09513ca1417099ce92257ef54b9d4cf3a7addc7dcbb1d6d848d307ee8a.js\",\n \"@hotwired/stimulus\": \"/assets/stimulus.min-dd364f16ec9504dfb72672295637a1c8838773b01c0b441bd41008124c407894.js\",\n \"@hotwired/stimulus-loading\": \"/assets/stimulus-loading-3576ce92b149ad5d6959438c6f291e2426c86df3b874c525b30faad51b0d96b3.js\",\n \"controllers/application\": \"/assets/controllers/application-368d98631bccbf2349e0d4f8269afb3fe9625118341966de054759d96ea86c7e.js\",\n \"controllers/hello_controller\": \"/assets/controllers/hello_controller-549135e8e7c683a538c3d6d517339ba470fcfb79d62f738a0a089ba41851a554.js\",\n \"controllers\": \"/assets/controllers/index-31a9bee606cbc5cdb1593881f388bbf4c345bf693ea24e124f84b6d5c98ab648.js\"\n }\n}</script>\n<link rel=\"modulepreload\" href=\"/assets/application-37f365cbecf1fa2810a8303f4b6571676fa1f9c56c248528bc14ddb857531b95.js\">\n<link rel=\"modulepreload\" href=\"/assets/turbo.min-f3765a09513ca1417099ce92257ef54b9d4cf3a7addc7dcbb1d6d848d307ee8a.js\">\n<link rel=\"modulepreload\" href=\"/assets/stimulus.min-dd364f16ec9504dfb72672295637a1c8838773b01c0b441bd41008124c407894.js\">\n<link rel=\"modulepreload\" href=\"/assets/stimulus-loading-3576ce92b149ad5d6959438c6f291e2426c86df3b874c525b30faad51b0d96b3.js\">\n<link rel
expect(response.body).to include(@tweet.text)
まとめ(結合テストのメソッド)
visit
指定したページへ遷移できる
visit Prefix名_path
# トップページに移動する
visit root_path
page
visitで訪れた先のページの見える分だけの情報が格納されている
カーソルを合わせてはじめて見ることができる文字列は含まれない
fill_in
fill_in 'フォームの名前', with: '入力する文字列'と記述することで、フォームへ入力できる
# ユーザー情報を入力する
fill_in 'Nickname', with: @user.nickname
fill_in 'Email', with: @user.email
fill_in 'Password', with: @user.password
fill_in 'Password confirmation', with: @user.password_confirmation
all
指定した要素を全て取得する
# pageに存在する同名のクラスを持つ要素をまとめて取得
all('クラス名')
# 「0番目のmoreクラス」を取得 ※添字なので、0からスタートする
all('クラス名')[0]
find
指定した要素を取得する
find('要素')
find().click
find('クリックしたい要素').clickと記述することで、実際にクリックできる
# <input type="submit" name="commit" value="Sign up" data-disable-with="Sign up">をクリックする
find('input[name="commit"]').click
find_link().click
find_link('リンクの文字列', href: 'URL').clickと記述することで、a要素で表示されているリンクをクリックする
hover
特定の要素にカーソルをあわせたときの動作を再現する
※カーソルを合わせる要素を特定するために、親要素のクラス名を指定する
find('[ブラウザ上の要素]').hover
# 実際に使用する場合
find('.[親要素のクラス名]').find('[ブラウザ上の要素]').hover
# user_navクラスの中にあるspan要素がログアウトボタン
# カーソルを合わせるとログアウトボタンが表示されることを確認する
expect(
find('.user_nav').find('span').hover
).to have_content('ログアウト')
click_onメソッド
引数に文字列を取り、一致するテキストなどを持った要素をクリックできるメソッド
※()をつけるかは自由
# 「<a href="/contents/1">詳しくはこちら</a>」の要素をクリックしたことにできる
click_on ('詳しくはこちら')
# 「<input type="submit" name="commit" value="ログイン">」の要素をクリックしたことにできる
click_on ('ログイン')
attach_fileメソッド
画像などのアップロード用のinput要素(タイプがfileのinput要素)に、テスト用画像を添付(アタッチ)できるメソッド
| 引数 | 内容 |
|---|---|
| 第一引数 | 画像をアップロードするinput要素のname属性の値 |
| 第二引数 | アップロードする画像のパス |
| 第三引数以降 | オプション |
attach_file('input要素のname属性の値', 画像のパス)
# 添付する画像を定義
image_path = Rails.root.join('public/images/test_image.png')
# 画像選択フォームに画像を添付する
attach_file('message[image]', image_path)
make_visible
CSSのdisplay: none;を設定していることで、非表示になっている画像選択フォームの場合に付与するオプション
# 非表示の画像選択フォームに画像を添付する
attach_file('message[image]', image_path, make_visible: true)
fixture_file_uploadメソッド
インスタンスに画像ファイルをセットするメソッド
current_path
現在いるページのパスを示す
# 今いるページが指定したURLであることを確認できる
expect(current_path).to eq [Prefix]_path
Ruby on Railsまとめ
