2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

RSpec 基本まとめ01(Model, FactoryBot, Controller)

Posted at

Everyday Rails - RSpecによるRailsテスト入門でRSpecの勉強を行っているので、今後見返すことができるようまとめてみました。

関連記事

describe, context, it の使い分け

  • describe : テストの対象を記述
  • context : 特定の条件を記述

モデルスペック

特徴

  • モデルをテストすればアプリケーションのコアとなる部分をテストできる ⇨ 土台が堅牢になり、信頼性の高いコードベースの構築につながる

RSpecファイル作成

# models/user.rbの場合

bin/rails g rspec:model user
spec/models/user_spec.rb
# RSpecに対し、ファイル内のテストを実行するためにRailsアプリケーションの読み込みが必要
require 'rails_helper'

RSpec.describe User, type: :model do
  pending "add some examples to (or delete) #{__FILE__}"
end

マッチャ

be_valid

モデルが有効な状態か検証

spec/models/user_spec.rb
# 例
.
.
it "is valid user" do
  user = User.new(
    name: 'test',
    email: 'tester@example.com',
  )
  expect(user).to be_valid
end

include

バリデーションのテスト時等に使用

spec/models/user_spec.rb
# 例
.
.
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

期待する値と一致しているか検証

例(性と名を連結して返すインスタンスメソッドをテスト)

app/models/users.rb
def name
  [firs_tname, last_name].join(' ')
end
spec/models/user_spec.rb
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

インストール

Gemfile
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
spec/factories/users.rb
FactoryBot.define do
  factory :user do
    first_name "test"
    last_name "user"
    email "tester@example.com"
    .
    .
  end
end

テスト

有効なファクトリか検証

spec/models/user_spec.rb
require 'rails_helper'

describe User do
  it "has a valid factory" do
    # インスタンス化されるだけで、保存はしていない
    expect(FactoryBot.build(:user)).to be_valid
  end
end

ファクトリを使用したバリデーションの検証

spec/models/user_spec.rb
# 名がなければ無効な状態であること
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

シーケンスを使ったユニークなデータ生成

spec/factories/users.rb
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つのモデル間の関連づけ

spec/factories/comments.rb
FactoryBot.define do
  factory :comment do
    comment "test comment"
    # ファクトリでの関連づけ
    association :post
    association :user
  end
end
spec/factories/posts.rb
FactoryBot.define do
  factory :post do
    sequence(:name) { |n| "Post #{n}" }
    # aliasを設定
    association :owner
  end
end
spec/factories/users.rb
FactoryBot.define do
  # aliasを設定
  factory :user, aliases: [:owner] do
    first_name "test"
    last_name "user"
    sequence(:email) { |n| "tester#{n}@example.com" }
  end
end

関連づけを行うと、下記のように子モデルのインスタンスを生成すると、その子に関連したpostuserが用意される

spec/models/comment_spec.rb
.
.
# ファクトリで関連データを生成
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に設定する

spec/factories/comments.rb
.
.
  association: posts
  user { project.owner }
end

ファクトリはまれに、想定していない不必要なデータが生成されていることがあるので注意が必要である。

また、aliasを設定すると、app/models/post.rbが以下のように変更されている。

app/models/post.rb
.
.
  belongs_to :owner, class_name: User, foreign_key: :user_id
  has_many :comments
.
end

be_late どう載せるか

trait

ファクトリの重複を減らす方法として、変更したい属性値だけをまとめて設定するtraitが存在する。

(例)締め切りが存在する仕事(task)をファクトリで生成

spec/factories/tasks.rb
.
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
.
spec/models/task_spec.rb
.
# 締め切り日が過ぎている
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つ生成

spec/factories/tasks.rb
.
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メソッドでは、モデルを作成するための関連モデルが必要

テスト

spec/models/task_spec.rb
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は認証が必要なコントローラのアクションに対して、ユーザーのログイン状態をシミュレートするヘルパーを提供しているため、設定を追加することで使用可能。

spec/ralis_helper.rb
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を用意して記述
spec/factories/posts.rb
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でフォーマットの形式を取得することができる
spec/controllers/comments_controller_spec.rb
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

参考

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?