#はじめに
業務でRspecを使用するため、RailsチュートリアルのMinitestの実装部分をRspecを用いて実装した。(実装範囲はRailsチュートリアル第3章〜第8章)
RailsチュートリアルではControllerやModelに関するテストと統合テスト(Integration Test)がある。
このIntegrationTestの部分にはRspecのRequestSpecを用いた。
また、コントローラの機能テストに関しては、Rails5以降ではrequest specで記述することが推奨されているため、こちらもRequestSpecを用いて実装した。
#コントローラー機能テストのポイント
以下がcontroller specで行うべきテスト項目です。
・Webリクエストが成功したか
・正しいページにリダイレクトされたか
・ユーザー認証が成功したか
・レスポンスのテンプレートに正しいオブジェクトが保存されたか
・ビューに表示されたメッセージは適切か
request specでも実装内容はほとんど同じであるが、
「レスポンスのテンプレートに正しいオブジェクトが保存されたか」は
コントローラの内部実装に関わる点はテストすべきではないとされている。
理由としてはrequest specはリクエスト/レスポンスにのみ関心を持つブラックボックステストであるといったところである。
#実装
Railsチュートリアルの第3章〜第8章では、
①Getメソッドでのページ(Home,About,Help,Contact,New)
②Postメソッドを用いたSignUp
③ログイン・ログアウトに関して
④Modelに関して
と、大きく分けて4つに分けられる。
まずは共通準備としてFactoryBotを使用。
FactoryBot.define do
factory :user do
name { "DefaultName" }
email { "defaultemail@gmail.com" }
password {"foobar"}
password_confirmation {"foobar"}
end
end
後に使用するHelperを定義。
module UsersHelper
def sign_in_as(user)
post login_path, params: { session: { email: user.email,password: user.password } }
end
end
①に関してのRequestSpec
一つのitに一つのテストを行うことを推奨されているのでそれに従う。
require 'rails_helper'
RSpec.describe "Getメソッドでのページ", type: :request do
describe "GET #Home" do
before do
get root_path
end
it "Homeページのhttpリクエストは正しいか" do
expect(response).to have_http_status(200)
end
it 'タイトルが正しく表示されているか' do
expect(response.body).to include "Ruby on Rails Tutorial Sample App"
end
end
describe "GET #About" do
before do
get about_path
end
it "Aboutページのhttpリクエストは正しいか" do
expect(response).to have_http_status(200)
end
it 'タイトルが正しく表示されているか' do
expect(response.body).to include "About"
end
end
describe "GET #Help" do
before do
get help_path
end
it "Helpページのhttpリクエストは正しいか" do
expect(response).to have_http_status(200)
end
it 'タイトルが正しく表示されているか' do
expect(response.body).to include "Help"
end
end
describe "GET #Contact" do
before do
get contact_path
end
it "Contactページのhttpリクエストは正しいか" do
expect(response).to have_http_status(200)
end
it 'タイトルが正しく表示されているか' do
expect(response.body).to include "Contact"
end
end
describe "GET #New" do
before do
get signup_path
end
it "User/newページのhttpリクエストは正しいか" do
expect(response).to have_http_status(200)
end
it 'タイトルが正しく表示されているか' do
expect(response.body).to include "Sign up"
end
end
end
続いて、②の新規登録に関してのRequestSpec
途中出てくるparams: { user: FactoryBot.attributes_for(:user,name: '') }
でだいぶ苦戦したのでここについて後述します。※1
require 'rails_helper'
RSpec.describe "SignUpに関して", type: :request do
describe "POST #create" do
context 'パラメータが妥当' do
it "リクエストが成功すること" do
post signup_path, params: { user: FactoryBot.attributes_for(:user) }
expect(response).to have_http_status(302)
end
it 'ユーザーが登録されること' do
expect do
post signup_path, params: { user: FactoryBot.attributes_for(:user) }
end.to change(User, :count).by(1)
end
it 'リダイレクトすること' do
post signup_path, params: { user: FactoryBot.attributes_for(:user) }
expect(response).to redirect_to User.last
end
end
context 'パラメータが不正な場合' do
it 'リクエストが成功すること' do
post signup_path, params: { user: FactoryBot.attributes_for(:user,name: '') }
expect(response).to have_http_status(200)
end
it 'ユーザーが登録されないこと' do
expect do
post signup_path, params: { user: FactoryBot.attributes_for(:user,name: '') }
end.to_not change(User, :count)
end
it 'エラーが表示されること' do
post signup_path, params: { user: FactoryBot.attributes_for(:user,name: '') }
expect(response.body).to include "error"
end
end
end
end
③ログイン・ログアウトに関してのRequestSpec
require 'rails_helper'
RSpec.describe "ログイン・ログアウトに関して", type: :request do
describe "GET #show" do
include UsersHelper
before do
@user=FactoryBot.create(:user)
end
describe '正常にログイン&ログアウト' do
it "ログイン成功" do
sign_in_as @user
get user_path(@user)
expect(response).to be_success
expect(response).to have_http_status(200)
end
it 'ログアウト' do
# 一旦ログイン
sign_in_as @user
get user_path(@user)
expect(response).to be_success
# ログアウト
delete logout_path
#be_falsey → nilか空白であればfalseです
expect(response).to have_http_status(302)
expect(session[:user_id]).to be_falsey # => nil
end
end
context "ログインに失敗した時" do
it "フラッシュメッセージの残留をキャッチすること" do
get login_path
expect(response).to have_http_status(:success)
#「email:""」と「password:""」の値を持ってlogin_pathにアクセス
# → バリデーションに引っかかりログインできない
post login_path, params: { session: { email: "", password: "" }}
expect(response).to have_http_status(:success)
#flash[:danger]が表示されているかチェック
expect(flash[:danger]).to be_truthy
#root_path(別ページ)に移動してflash[:danger]が表示されていないかチェック
get root_path
expect(flash[:danger]).to be_falsey
end
end
end
end
④Modelに関してのRspec
- 存在性
- 長さ
- フォーマット
- 一意性
- パスワードの確認
と項目を分けていることに注意。
require 'rails_helper'
RSpec.describe User, type: :model do
before do
@user = FactoryBot.create(:user)
end
describe '存在性' do
it '@userが有効であることの確認' do
expect(@user).to be_valid
end
it 'nameが空白は無効' do
@user.name = " "
expect(@user).to be_invalid
end
it "emailが空白は無効" do
@user.email = " "
expect(@user).to be_invalid
end
it "password空白は無効" do
@user.password = @user.password_confirmation = " " * 6
expect(@user).to be_invalid
end
end
describe '長さ' do
it "name51文字以上は無効" do
@user.name = "a" * 51
expect(@user).to be_invalid
end
it "name50文字以下は有効" do
@user.name = "a" * 50
expect(@user).to be_valid
end
it "emailが256字以上は無効" do
@user.email = "a" * 244 + "@example.com"
expect(@user).to be_invalid
end
it "emailが255字以下は有効" do
@user.email = "a" * 243 + "@example.com"
expect(@user).to be_valid
end
it "password5字以下は無効" do
@user.password = @user.password_confirmation = "a" * 5
expect(@user).to be_invalid
end
it "password 6字以上は有効" do
@user.password = @user.password_confirmation = "a" * 6
expect(@user).to be_valid
end
end
describe 'フォーマット' do
it "emailは規定の表記でなければ無効" do
invalid_addresses = %w[user@example,com foo@bar..com user_at_foo.org user.name@example. foo@bar_baz.com foo@bar+baz.com]
invalid_addresses.each do |invalid_address|
@user.email = invalid_address
expect(@user).to be_invalid,"#{invalid_address.inspect}が無効ではありません"
end
end
it "emailは規定の表記であれば有効" do
valid_addresses = %w[user@example.com USER@foo.COM A_US-ER@foo.bar.org first.last@foo.jp alice+bob@baz.cn]
valid_addresses.each do |valid_address|
@user.email = valid_address
expect(@user).to be_valid, "#{valid_address.inspect} が有効ではありません"
end
end
end
describe '一意性' do
it "emailの重複は無効" do
duplicate_user = @user.dup
duplicate_user.email = @user.email.upcase
@user.save!
expect(duplicate_user).to be_invalid
end
end
it "emailを小文字に変換後の値と大文字を混ぜて登録されたアドレスが同じか" do
#わかりやすくベタ書き
@user.email = "Foo@ExAMPle.CoM"
@user.save!
#全て小文字のemailと等しいかのテスト
expect(@user.reload.email).to eq "foo@example.com"
end
describe 'passwordとpassward_comfirmの機能チェック' do
it "一致する場合" do
expect(@user).to be_valid
end
it "一致しない場合" do
user = User.new(name: 'false',email: 'false@gmail.com', password: "password", password_confirmation: "different")
user.valid?
expect(user.errors[:password_confirmation]).to include("doesn't match Password")
end
end
end
以上でRailsチュートリアルの第3章〜第8章のMinitestをRspecでの書き換えを網羅できたと思われる。
#FactoryBotのcreateとbuildについて
Rspecでの作成の際にFactoryBotでcreateを使うべきか、buildを使うべきか迷う場面があった。
○ build
・DB上にはデータがないので、DBにアクセスする必要があるテストのときは使えない。
・DBにアクセスする必要がないので処理が比較的軽くなる。
○ create
・DB上にデータを作成する。
・DBにアクセスする処理のときは必須。
今回はDB上のデータを使用するためcreateを用いた。
#FactoryBot.attributes_forに関して※1
こちらはじめは次のように実装していた。
post signup_path, params: params: { name:'test',email: 'test@gmail.com', password: 'aaaaaa', password_confirmation: 'aaaaaa' }
すると次のようなエラーになってしまう。
Failure/Error: params.require(:user).permit(:name, :email, :password,:password_confirmation)
ActionController::ParameterMissing:
param is missing or the value is empty: user
このエラーについて色々調べていく中でattributes_forメソッドを使った書き方にいきついた。
ですが、これを用いずに最初に書こうとしたやり方でparam is missing
にならない正しい書き方も知りたいので、助言いただけると助かります。
#まとめ
RequestSpecでの実装はMinitestのassertと一対一で書き換えられるものではないため、それに相当する書き方を模索するのに苦労した。
これに慣れるためには、Rspecのマッチャの学習をより進めていくことが近道なのだろう。
RequestSpecを用いるか、それともFeatureSpecを用いるかどちらの方がよいのだろうかと長時間悩んでいたが、実際はどちらでも可能というのが結論であった。
ただ、Railsチュートリアルの統合テストに関してはRequestSpecでの書き換えのほうが感覚的は容易であると書かれている。※2
#参考
※2
Junichi Ito
「【動画付き】Railsチュートリアルの統合テスト(integration test)は、RSpecのリクエストスペックに置き換えるのがラクです」より
https://qiita.com/jnchito/items/8d8d579cdca131c0db14