別タブで開いた画面のテスト / ApplicationHelperのメソッドをRSpecでも使用
別タブで開いた画面をテストする方法
下記のコードのようにUserの一覧ページからUserの詳細ページに移るときにtarget: :_blank
を使って別タブで表示するような実装をしている部分のテストを書いていきます。
<%= link_to "詳細", users_path(@user), target: :_blank, rel: "noopener noreferrer" %>
rel: "noopener noreferrer"
この記述はセキュリティの問題を回避するために必要です。
参考 : 【Rails Tutorial】target="_blank"で新しいページを開くときのセキュリティ対策|Artefact|note
別タブのテストを抜粋して、テストコードを書いていきます。
※事前にuserのFactoryBotとsystem/user_spec.rbファイルを作成しておきます。
RSpec.describe 'User', type: :system do let!(:user) { create(:user) } describe 'User一覧' do context '正常系' do it 'User一覧ページから詳細ページにアクセスできること' do visit users_path click_link '詳細' switch_to_window(windows.last) expect(page).to have_content user.name expect(current_path).to eq users_path(user) end end end end
switch_to_window(windows.last)
最後に開いたページを別タブで開くことができるコードです。ここでいう最後に開いたページはclick_link '詳細'
で開いたページになります。
参考 : Capybaraと仲良くなる(タブ・ウィンドウの操作について) - Qiita
ApplicationHelperのメソッドをRSpecでも使用する方法
application_helper.rbファイルにはビューで使用したい機能のために追加していくメソッドを置いてあります。ここで定義したメソッドのことをヘルパーメソッドと呼びます。
例えば時間の表記を指定するメソッドを作成してみます。Time.current
を使用すると2021/04/22 10:00:00
のようにとても細かい表記になってしまいます。そのためわかりやすい表記に変えるためにstrftimeを使用してカスタマイズしています。
参考 : Time#strftime (Ruby 3.0.0 リファレンスマニュアル)
helpers/application_helper.rb module ApplicationHelper def short_time(datetime) datetime.strftime("%-m/%d %-H:%M") end end
ビューで使用するために作ったこのメソッドをRSpecで使うには、spec/rails_helper.rbでApplicationHelperをincludeするだけです!
RSpec.configure do |config| include ApplicationHelper end
これで全てのspecファイルにてapplication_helper.rbで定義しているメソッドを使用することができるようになります。
System Spec
システムスペックとは
複数のモデル、コントローラを統合したプログラム全体の挙動をチェックする統合テストです。またJavaScriptを利用する画面のテストを書くこともできます。
システムスペックには下記の機能がデフォルトで準備されています。
selnium-webdriverとはブラウザを自動で操作してテストする機能です。
トランザクション管理とは一連のSQL文をセットで扱い、全て正常に完了したら全テーブルの変更を「確定」し、万が一途中でエラーが発生したら全テーブルの変更を「破棄」するという意味です。
システムスペックの準備
gemの追加
capybaraとwebdriversというgemをbundle install
します。
railsのテスト環境でのみ使えるようにします。
group :test do gem 'capybara' gem 'webdrivers' end
capybaraを使うことでリンクをクリックしたり、webフォームを入力したり、画面の表示を検証することができるようになります。
webdriversはselnium-webdriverとchromedriver-helperの代替です。Chromeを使用してテストを動かすことができます。
capybaraの初期設定
capybaraの設定ファイルを作成します。
$ mkdir spec/support $ touch spec/support/capybara.rb
作成したファイルにテストの設定を記入します。
spec/support/capybara.rb RSpec.configure do |config| config.before(:each, type: :system) do driven_by :selenium, using: :headless_chrome end end
using: :headless_chrome
を指定することで、テスト毎に開かれるブラウザを非表示にすることができます。
上記のファイルをspec/spec_helper.rbに読み込みます。
require 'support/capybara'
ログイン処理をmoduleを作成して共通化
spec/supportディレクトリ配下にlogin_module.rbファイルを作成します。
$ touch spec/support/login_module.rb
作成したファイルの中身を記入していきます。
module LoginModule def Login_as(user) visit root_path click_link 'Login' fill_in "Email", with: user.email fill_in "Password", with: 'password' click_button "Login" end end
rails_helper.rbにて作成したファイルを読み込みます。
RSpec.configure do |config| config.include LoginModule end
rails_helper.rbの23行目あたりの一文をコメントアウトします。
これはspec/support配下のファイルを読み込むための設定です。
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }
実行するテストケースを限定
spec_helper.rbの50行目あたりの一文を有効にします。
config.filter_run_when_matching :focus
この設定をすることで実行したいテストブロックにfをつけることで、そのブロックのみ実行することができます。下記ではitにfをつけることで他にもたくさんあるテストの中からこの一つのテストケースだけテストすることができるようになります。
fit 'タスクの新規作成ページに遷移しない' do visit new_task_path expect(current_path).to eq login_path expect(page).to have_content "Login required" end
システムスペックを実装
まず最初にgeneratorを使って実行ファイルを作成していきます。
$ rails generate rspec:system users
テストケースを作成して中身を記入していきます。
describeには一連の操作によって達成したい機能や処理の名前を記述します。
contextにはテストの内容を状況ごとに分類するために利用します。
itには末端のテストケースを記入します。
beforeにはitを実行する前にbeforeのブロックに書かれたコードを実行します。
require 'rails_helper' RSpec.describe "Users", type: :system do let(:user) { create(:user) } describe 'ログイン後' do before { Login_as(user) } describe 'ユーザー編集' do context 'フォームの入力値が正常' do it 'ユーザーの編集が成功する' do visit edit_user_path(user) fill_in 'Email', with: 'test@example.com' fill_in 'Password', with: 'test' fill_in 'Password confirmation', with: 'test' click_button 'Update' expect(current_path).to eq user_path(user) expect(page).to have_content "User was successfully updated." end end end end end
分量が多いのでテストケースを一つだけ用意しました。
let(:user) { create(:user) }
はFactoryBotで定義したuserを使用しています。let
を使うことで、最初にメソッドが呼ばれたときに評価されます。ここではLogin_as(user)
で呼ばれています。※let!
を使うと各itが実行される直前に評価されます。
またcreate_listメソッドを使うと複数のインスタンスをまとめて作成することもできます。
let(:user1) { create(:user) } let(:user2) { create(:user) } #このように書き換えることもできます。 => users = create_list(:user, 2)
before { Login_as(user) }
でlogin_module.rbで定義したLogin_asメソッドを使用しています。fill_in
やclick_button
、expect(page)
などはcapybaraが用意しているメソッドです。
参考 : Capybaraメモ - Qiita
応用編課題6で参照にしたサイト
punditのReadMe
GitHub - varvet/pundit: Minimal authorization through OO design and pure Ruby classes
punditについて
Pundit + Railsで認可の仕組みをシンプルに作る - Qiita
Authorization of Pundit - Qiita
403エラーページのファイルをpublic配下のものを使うとき
Model Spec
モデルスペックに含めるテスト
有効な属性で初期化された場合は、モデルの状態が有効になっていること。正常系のテスト。
バリデーションを失敗させるデータであれば、モデルの状態が無効になっていること。異常系のテスト。
クラスメソッドとインスタンスメソッド、スコープが期待通りに動作すること。
モデルスペックを作成する
※ 前提条件としてRSpecとFactoryBotをインストールしていること。
$ rails generate rspec:model user
上記のコマンドで生成されるファイル
① spec/models/user_spec.rb
② spec/factories/users.rb
FactoryBotのgemが入っていると自動的に②のファイルが作られます。
spec/models/user_spec.rbのファイルの中身を確認します。
require 'rails_helper' RSpec.describe User, type: :model do pending "add some examples to (or delete) #{__FILE__}" end
require 'rails_helper'
はテストスイート内のほぼ全てのファイルで必要です。この記述によってRSpecに対して、ファイル内のテストを実行するためにRailsアプリケーションの読み込みが必要であると伝えています。
FactoryBotを使ってテストデータを作成します。FactoryBotに関してはこちらを参照してください。
factories/users.rb FactoryBot.define do factory :user do sequence(:email) { |n| "user_#{n}@example.com" } password { "password" } password_confirmation { "password" } end end
準備は完了したので、Userモデルのバリデーションに関するテストを作成します。
まずテストケースを作成していきます。
テストケースは英語でも日本語でもわかりやすく書いていればどちらでも大丈夫です。
require 'rails_helper' RSpec.describe User, type: :model do describe 'validation' do it "メール、パスワードがあれば有効な状態であること" it "メールアドレスがなければ無効な状態であること" it "重複したメールアドレスなら無効な状態であること" it '異なるメールアドレスなら有効な状態であること' end end
describe
はテストのグループ化宣言です。
モデルスペックではモデルのクラス名やメソッド名などを記入します。
it
はテストをexampleという単位にまとめる役割です。
describe
の中には複数のexample(it
)を書くことができます。
それではテストケースを実装していきます。
require 'rails_helper' RSpec.describe User, type: :model do describe 'validation' do it "メール、パスワードがあれば有効な状態であること" do user = build(:user) expect(user).to be_valid expect(user.errors).to be_empty end it "メールアドレスがなければ無効な状態であること" do user_without_email = build(:user, email: '') expect(user_without_email).to be_invalid expect(user_without_email.errors[:email]).to eq ["can't be blank"] end it "重複したメールアドレスなら無効な状態であること" do user = create(:user) user_with_duplicated_email = build(:user, email: user.email) expect(user_with_duplicated_email).to be_invalid expect(user_with_duplicated_email.errors[:email]).to eq ["has already been taken"] end it '異なるメールアドレスなら有効な状態であること' do user = create(:user) user_with_another_email = build(:user, email: 'another_email') expect(user_with_another_email).to be_valid expect(user_with_another_email.errors).to be_empty end end end
上記のコードで出てきたbe_valid
やeq
などはマッチャというものです。
expect(user).to be_valid
このコードのto
は「~であること」 を期待しているという意味です。
be_valid
はuser.valid?
がtrue
であるということを意味しています。
expect(user_without_email.errors[:email]).to eq ["can't be blank"]
このコードのeq
は「等しい」かどうかを検証しますという意味です。
FactoryBotについて
FactoryBotとは
テストデータ生成のためのデータライブラリのことです。
ここで作成したデータはテストケースで呼び出すことができます。
FactoryBotの使い方
GemFileにFactoryBotを追加します。
開発環境とテスト環境のみで使うものなので、下記のように指定してbundle install
します。
group :development, :test do gem 'factory_bot_rails' end
factoriesディレクトリの配下にUserモデルのデータファイルを生成します。
$ bin/rails g factory_bot:model user
まずusersテーブルのカラムを確認しました。
db/schema.rb create_table "users", force: :cascade do |t| t.string "email", null: false t.string "crypted_password" t.string "salt" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["email"], name: "index_users_on_email", unique: true end
これを見るとemailカラムにはnull: false
とunique: true
があります。なのでemailは絶対必要なデータであり、同じEmailアドレスは重複して登録されてはいけません。
このことを踏まえてテストデータを作成していきます。
FactoryBot.define do factory :user do sequence(:email) { |n| "user_#{n}@example.com" } password { "password" } password_confirmation { "password" } end end
unique制約のあるカラムは、sequenceを使って重複したデータが作成されないようにします。
補足ですがsequenceにはもう一つの書き方があります。
sequence(:name){|n| "name_#{n}"}
sequence(:name, "name_1")
※ sequence (:email, "user_1@example.com")
とすると、末尾の数値や文字を増やしてしまうので、この場合"user_1@example.con"
となってしまい、想定していた結果(数字を増やすこと)を取得できません。
参考 : FactoryBot (旧FactoryGirl) の sequence と .next - Qiita
早速テストケースで作成したFactoryBotを使ってみます。
user = FactoryBot.create(:user)
これでuserのデータを作成することができます。
FactoryBot.の省略方法
設定を追加することで、FactoryBot.
の部分を省略することもできます。
spec/rails_helper.rb RSpec.configure do |config| config.include FactoryBot::Syntax::Methods end
この設定をすることで
user = create(:user)
FactoryBotの記述が省略できました。
モデルのアソシエーションを組んでいる場合
UserモデルとTaskモデルがあったとします。userはたくさんのtaskを持っている、という一対多の関係性があった時、taskのFactoryにアソシエーションを定義すると、テストでわざわざuserのテストデータを呼び出す必要がなくなります。
factories/tasks.rb FactoryBot.define do factory :task do sequence(:title, "title_1") content { "content" } status { :todo } deadline { 1.week.from_now } association :user end end
このように定義しておくと、
user = create(:user) task = create(:task)
ではなく、
task = create(:task)
taskのデータを作成すれば、userのデータも一緒に作成されます。
参考 : FactoryBotのassociationとは - Qiita
createとbuildの違い
it 'is invalid with a duplicate title' do task = create(:task) task_with_duplicated_title = build(:task, title: task.title) expect(task_with_duplicated_title).to be_invalid expect(task_with_duplicated_title.errors[:title]).to eq ["has already been taken"] end
ここでcreateとbuildをどうやって使い分けているのか疑問に思いました。
create
テストデータをデータベース上に保存して、データを永続化させます。
build
テストデータをメモリ上のみで記録します。
このテストケースではタイトルが重複しているのは無効というのを調べています。
まず最初のコードでtask
を作り、二番目のコードでtask
と同じタイトルを持つtask_with_duplicated_title
を作ります。
この場合task
のデータはtask_with_duplicated_title
と比較しなくてはいけないので持続的に保持しておく必要がありますよね。なのでここではcreateを使っています。
応用編課題5で参照にしたサイト
ActiveModelについて
form_objectについて
form objectを使ってみよう - メドピア開発者ブログ
ActiveModel::Attributesの使い方
[Rails] ActiveModel::Attributesの使い方(配列化やネストしたhashの取り扱いなども) - Qiita
【Rails】「ActiveModel::Attributes」が便利という話 - 日々の学びのアウトプットするブログ
ActiveModel::Attributesを使う - Qiita
検索機能について
【Rails】一覧ページ上部に検索機能を実装する ~ form_with ~ - Qiita
内部結合について
Rails における内部結合、外部結合まとめ - Qiita
【Rails】joinsメソッドのテーブル結合からネストまでの解説書 | Pikawaka - ピカ1わかりやすいプログラミング用語サイト
回答の解説を読んで
transientについて
FactoryGirlのtransientとtraitを活用する - Qiita
after[:build] コールバックについて
RSpecにおけるFactoryGirlの使い方まとめ - Qiita
FactoryBot(旧FactoryGirl)で関連データを同時に生成する方法いろいろ - Qiita
create_listについて
RSpecでFactoryBotから複数のインスタンスをまとめて作成する【create_listを使用】 - Qiita
応用編課題4で参照にしたサイト
enumについて
【Rails】enumチュートリアル | Pikawaka - ピカ1わかりやすいプログラミング用語サイト
時間選択の部分、一時間おきに選択できるようにする。
Rails で datetimepicker を使用する手順 - blog @arfyasu
FatControllerについて
【Ruby on Rails】Fat Controllerの解消〜まずはロジックをモデルへ〜 - Qiita
wheneverについて
[初学者]whenever を使って定期的にバッチ処理を行う(公開設定編) - Qiita
Wheneverは導入が超簡単なcrontab管理ライブラリGemです![Rails 4.2 x Ruby 2.3] | 酒と涙とRubyとRailsと
clonに関して
Wheneverを使ってRailsタスクを定期実行する - とりとめも
Railsで定期的にバッチ回す「Whenever」 - Qiita
find_eachに関して
[Rails]find_eachが無限ループして本番環境のメモリを食いつぶした話 - Qiita
RSpecでselect boxを使うとき
RSpec - rspecでselect box の value を指定して選択状態にしたい|teratail
回答の解説を読んで
assign_attriblutesについて色々な更新メソッドの知識
Active Recordのattributesの更新メソッド | 酒と涙とRubyとRailsと
ActiveRecord の attribute 更新方法まとめ - Qiita
Rails 3.1: assign_attributesメソッド - Rails 雑感 - Ruby on Rails with OIAX
scopeのwhereで日付の範囲指定をするときの書き方。
scope :past_published, -> { where('published_at <= ?', Time.current) }について。
[WIP][基礎編]scopeを使って可読性と保守性を向上 - Qiita
find_each(&: published!)の&:について
[初心者向け] RubyやRailsでリファクタリングに使えそうなイディオムとか便利メソッドとか - Qiita
単一テーブル継承(STI)について
【Rails】単一テーブル継承(STI)について - Qiita
みんなRailsのSTIを誤解してないか!? - Qiita
時間指定の時に使うagoとsinceについて