Everyday Rails - RSpecによるRailsテスト入門でRSpecの勉強を行っているので、今後見返すことができるようまとめてみました。
関連記事
describe, context, it の使い分け
- describe : テストの対象を記述
- context : 特定の条件を記述
モデルスペック
特徴
- モデルをテストすればアプリケーションのコアとなる部分をテストできる ⇨ 土台が堅牢になり、信頼性の高いコードベースの構築につながる
RSpecファイル作成
# models/user.rbの場合
bin/rails g rspec:model user
# RSpecに対し、ファイル内のテストを実行するためにRailsアプリケーションの読み込みが必要
require 'rails_helper'
RSpec.describe User, type: :model do
pending "add some examples to (or delete) #{__FILE__}"
end
マッチャ
be_valid
モデルが有効な状態か検証
# 例
.
.
it "is valid user" do
user = User.new(
name: 'test',
email: 'tester@example.com',
)
expect(user).to be_valid
end
include
バリデーションのテスト時等に使用
# 例
.
.
it "is invalid without an email" do
user = User.new(
name: 'test',
email: nil,
)
# 有効でないカラムに、error内容が格納される
user.valid?
expect(user.errors[:email]).to include("can't be blank")
end
*必要な値が空:"can't be blank"
*値が重複している:"has already been taken"
バリデーションのテストを追加することで、テストを書いている最中に、モデルが持つべきバリデーションについて考える機会を設けることができる。
be_empty
空の値であるか検証
eq
期待する値と一致しているか検証
例(性と名を連結して返すインスタンスメソッドをテスト)
def name
[firs_tname, last_name].join(' ')
end
it "returns a user's full name as a string" do
user = User.new(
first_name: 'test',
last_name: 'user',
email: 'tester@example.com'
)
expect(user.name).to eq 'test user'
end
同様にクラスメソッドやスコープもテスト可能
to ⇨ to_not / not_to
期待する値を反転させる
to_not / not_to に違いはない。どちらの方が文章として適切かで使い分けてOK
FactoryBot
インストール
group :development, :test do
gem "rspec-rails"
gem "factory_bot_rails"
.
end
bundle install
新規ファクトリファイルの生成
# 例 spec/factories/users.rb
bin/rails g factory_bot:model user
FactoryBot.define do
factory :user do
first_name "test"
last_name "user"
email "tester@example.com"
.
.
end
end
テスト
有効なファクトリか検証
require 'rails_helper'
describe User do
it "has a valid factory" do
# インスタンス化されるだけで、保存はしていない
expect(FactoryBot.build(:user)).to be_valid
end
end
ファクトリを使用したバリデーションの検証
# 名がなければ無効な状態であること
it "is invalid without a first name" do
# 検証対象をnilに置き換え
user = FactoryBot.build(:user, first_name: nil)
user.valid?
expect(user.errors[:first_name]).to include("can't be blank")
end
シーケンスを使ったユニークなデータ生成
FactoryBot.define do
factory :user do
first_name "test"
last_name "user"
sequence(:email) { |n| "tester#{n}@example.com" }
end
end
複数userが同じ値を持たないよう設定したい場合は、上記のコードのようにカウンタの値を1つずつ増やすことで、ユニークな値を生成することができる
ファクトリで関連づけ
(例) user
post
comment
の3つのモデル間の関連づけ
FactoryBot.define do
factory :comment do
comment "test comment"
# ファクトリでの関連づけ
association :post
association :user
end
end
FactoryBot.define do
factory :post do
sequence(:name) { |n| "Post #{n}" }
# aliasを設定
association :owner
end
end
FactoryBot.define do
# aliasを設定
factory :user, aliases: [:owner] do
first_name "test"
last_name "user"
sequence(:email) { |n| "tester#{n}@example.com" }
end
end
関連づけを行うと、下記のように子モデルのインスタンスを生成すると、その子に関連したpost
とuser
が用意される
.
.
# ファクトリで関連データを生成
it "generates associated data from a factory" do
comment = FactoryBot.create(:comment)
end
しかし、このままでは、userが2人生成されている。
なぜなら、comment
生成時にその親であるpost
が生成、次にそのpostの親であるuser1=owner
が生成される。
その後に、spec/factories/comments.rb
で設定した、association :user
によって、user2
が生成される
この問題の対処法は、下記のコードのように変更を加え、userを関連づけで生成されたpost.owner
に設定する
.
.
association: posts
user { project.owner }
end
ファクトリはまれに、想定していない不必要なデータが生成されていることがあるので注意が必要である。
また、alias
を設定すると、app/models/post.rb
が以下のように変更されている。
.
.
belongs_to :owner, class_name: User, foreign_key: :user_id
has_many :comments
.
end
be_late どう載せるか
trait
ファクトリの重複を減らす方法として、変更したい属性値だけをまとめて設定するtrait
が存在する。
(例)締め切りが存在する仕事(task)をファクトリで生成
.
factory :task do
sequence(:name) { |n| "Task #{n}" }
due_on 1.week.from_now
association :owner
# 昨日が締め切りのプロジェクト
trait :due_yesterday do
due_on 1.day.ago
end
# 今日が締め切りのプロジェクト
trait :due_today do
due_on Date.current.in_time_zone
end
# 明日が締め切りのプロジェクト
trait :due_tomorrow do
due_on 1.day.from_now
end
end
.
.
# 締め切り日が過ぎている
it " is late when the due date is past today" do
task = FactoryBot.create(:task, :due_yesterday)
expect(task).to be_late
end
# 締め切り日が今日でスケジュール通り
it "is on time when the due date is today" do
task = FactoryBot.create(:task, :due_today)
expect(task).to_not be_late
end
# 締め切り日がまだ先ならスケジュール通り
it "is on time when the due date is in the future" do
task = FactoryBot.create(:task, :due_tomorrow)
expect(task).to_not be_late
end
.
コールバック
ファクトリでもcallbackを使用することができる
(例)task生成時に、memoを3つ生成
.
factory :task do
sequence(:name) { |n| "Task #{n}" }
due_on 1.week.from_now
association :owner
# memo付きのtask
trait :with_notes do
after(:create) { |task| create_list(:note, 3, task: task) }
end
.
end
.
create_listメソッドでは、モデルを作成するための関連モデルが必要
テスト
it "can have many notes" do
task = FactoryBot.create(:task, :with_notes)
expect(task.notes.length).to eq 3
end
詳しい使い方は、
コントローラスペック
コントローラのテストは公式に格下げされた。統合テストに置き換えることが推奨されるが、基本を勉強する上では、学びがあるためまとめてみます。
ファイル生成
# 例
bin/rails g rspec:controller post
テスト例
正常にレスポンスを返すことを検証 & 200レスポンスを返すことを検証
-
response
: ブラウザに返すべきアプリケーションの全データを保持しているオブジェクト -
be_success
: レスポンスステータスが成功(200)か、それ以外であるかをチェック
describe "#index" do
it "responds successfully" do
get :index
expect(response).to be_success
end
it "returns a 200 response" do
get :index
expect(response).to have_http_status "200"
end
end
認証(Devise)が必要なテスト
Deviseは認証が必要なコントローラのアクションに対して、ユーザーのログイン状態をシミュレートするヘルパーを提供しているため、設定を追加することで使用可能。
RSpec.configure do |config|
.
.
# コントローラスペックで、Deviseのテストヘルパーを使用できるよう設定
config.include Devise::Test::ControllerHelpers, type: :controller
end
上記のように設定することで、sign_in
ヘルパーを使用することができるようになる。
.
before do
@user = FactoryBot.create(:user)
end
# 認証済みのユーザー
it "responds successfully" do
sign_in @user
get :index
expect(response).to be_success
end
# ゲストユーザー
context "as a guest" do
# 302レスポンスを返すことを検証
it "returns a 302 response" do
get :index
expect(response).to have_http_status "302"
end
# サインイン画面にリダイレクトすること
it "redirects to the sign-in page" do
get :index
expect(response).to redirect_to "./users/sign_in"
end
end
-
redirect_to
: コントローラが認証されていないリクエストの処理を中断し、指定した画面に移動することを検証
*has_secure_passwordメソッドを使用している場合の例
def sign_in(user)
cookies[:auth_token] = user.auth_token
end
showアクションのテスト
#index
との違いは、GETリクエストの際にparamsを設定する点
describe "#show" do
.
before do
@user = FactoryBot.create(:user)
@post = FactoryBot.create(:post, owner: @user)
end
.
it "responds successfully" do
sign_in @user
get :show, params: { id: @post.id }
expect(response).to be_success
end
.
end
POST
- POSTの際は、paramsを渡す
-
attributes_for(:post)
は、プロジェクトファクトリからテスト用の属性値をハッシュとして作成する
describe "#create" do
before do
@user = FactoryBot.create(:user)
end
it "adds a post" do
post_params = FactoryBot.attributes_for(:post)
sign_in @user
expect {
post :create, params: { post: post_params }
}.to change(@user.posts, :count).by(1)
end
end
PATCH
- PATCHの時も、更新するオブジェクトのidと、変更する値を渡す
-
reload
メソッドを使ってDB上の値を更新しないと、メモリに保存された値が再利用されて李舞、値の変更が反映されないので注意
describe "#update" do
before do
@user = FactoryBot.create(:user)
@post = FactoryBot.create(:post, owner: @user)
end
it "updates a post" do
post_params = FactoryBot.attributes_for(:post, name: "New Post Name")
sign_in @user
patch :update, params: { id: @post.id, post: post_params }
expect(@post.reload.name).to eq "New Post Name"
end
end
DESTROY
- destroyメソッドには、DELETEリクエストでアクセス
- DESTROYの際も、削除するオブジェクトのidをparamsで渡す
describe "#destroy" do
before do
@user = FactoryBot.create(:user)
@post = FactoryBot.create(:post, owner: @user)
end
it "deletes a post" do
sign_in @user
expect {
delete :destroy, params: { id: @posts.id }
}.to change(@user.posts, :count).by(-1)
end
end
ユーザー入力のエラーをテスト
- テストでは、何か正しくないことが怒った時も意図した通りの動きになるかを検証するべきである。
- 今回は無効の値を設定する
trait
を用意して記述
FactoryBot.define do
factory :post do
sequence(:name) { |n| "Test Project #{n}" }
description "Sample project for testing purposes"
due_on 1.week.from_now
association :owner
.
.
trait :invalid do
name nil
end
end
end
before do
@user = FactoryBot.create(:user)
end
it "does not add a post" do
post_params = FactoryBot.attributes_for(:post, :invalid)
sign_in @user
expect {
post :create, params: { post: post_params }
}.to_not change(@user.posts, :count)
end
HTML以外の出力
- ここまでのテストしたコントローラのアクションは全て
text/html
フォーマット - 明示的に記述することで、JSON形式でレスポンスを返すことをテストすることができる
-
response.content_type
でフォーマットの形式を取得することができる
before do
@user = FactoryBot.create(:user)
@post = FactoryBot.create(:post, owner: @user)
@comment = @post.comments.create!(name: "Test Comment")
end
describe "#show" do
# JSON形式でレスポンスを返すことを検証
it "responds with JSON formatted output" do
sign_in @user
# フォーマットを指定
get :show, format: :json,
params: { post_id: @post.id, id: @comment.id }
expect(response.content_type).to eq "application/json"
end
end