学習記録

アウトプット用に作りました

別タブで開いた画面のテスト / 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_inclick_buttonexpect(page)などはcapybaraが用意しているメソッドです。
    参考 : Capybaraメモ - Qiita

応用編課題6で参照にしたサイト

punditのReadMe

GitHub - varvet/pundit: Minimal authorization through OO design and pure Ruby classes

punditについて

punditを使ってみる - maru877’s blog

Pundit + Railsで認可の仕組みをシンプルに作る - Qiita

Authorization of Pundit - Qiita

403エラーページのファイルをpublic配下のものを使うとき

Rails 5の404/500エラーページ、簡単作成手順 | 酒と涙とRubyとRailsと

Railsで404エラーメッセージを出すために - Qiita

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_valideqなどはマッチャというものです。

expect(user).to be_valid

このコードのtoは「~であること」 を期待しているという意味です。
be_validuser.valid?trueであるということを意味しています。

expect(user_without_email.errors[:email]).to eq ["can't be blank"]

このコードのeqは「等しい」かどうかを検証しますという意味です。

参考 : 使えるRSpec入門・その2「使用頻度の高いマッチャを使いこなす」 - Qiita

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: falseunique: 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を使っています。

参考 : FactoryBotにおけるcreateとbuildの違い - Qiita

応用編課題5で参照にしたサイト

ActiveModelについて

Active Model の基礎 - Railsガイド

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についてまとめておく - Qiita

【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) }について。

部分一致とか日付の範囲で検索したい - Qiita

[WIP][基礎編]scopeを使って可読性と保守性を向上 - Qiita

find_each(&: published!)の&:について

[初心者向け] RubyやRailsでリファクタリングに使えそうなイディオムとか便利メソッドとか - Qiita

単一テーブル継承(STI)について

【Rails】単一テーブル継承(STI)について - Qiita

みんなRailsのSTIを誤解してないか!? - Qiita

時間指定の時に使うagoとsinceについて

RubyとRailsにおけるTime, Date, DateTime, TimeWithZoneの違い - Qiita