RSpecとは

Rubyにおけるテスティイングフレームワークのことです。

Rubyでは標準でMinitestという別のフレームワークがあります。Ruby on RailsチュートリアルでもMinitestでテスト方法を教わります。
ですが、実際に現場で使われているテスティイングフレームワークはRSpecが圧倒的に多いようです。

本記事ではRSpecをインストールし、既に定義されているモデルに対してバリデーションテストするまでを記載します。

使用技術のバージョン

・Ruby: 2.6.4
・Ruby on Rails: 5.2.3

モデルの概要

今回取り扱うモデルはuserモデルとtaskモデルです。

[userモデル]

app/models/user.rb
class User < ApplicationRecord

  has_many :tasks, dependent: :destroy

  validates :password, length: { minimum: 3 }, if: -> { new_record? || changes[:crypted_password] }
  validates :password, confirmation: true, if: -> { new_record? || changes[:crypted_password] }
  validates :password_confirmation, presence: true, if: -> { new_record? || changes[:crypted_password] }
  validates :email, uniqueness: true, presence: true

end

[taskモデル]

app/models/task.rb
class Task < ApplicationRecord
  belongs_to :user
  validates :title, presence: true, uniqueness: true
  validates :status, presence: true
  enum status: { todo: 0, doing: 1, done: 2 }
end

今回はtaskモデルのバリデーションをテストするため、userモデルのバリデーションの説明は割愛します。
taskモデルから分かる通り、titleカラムとstatusカラムにバリデーション(制限)がかけられています。

titleカラム
presence: true => 値が必ず格納されていなければならない
uniqueness: true => 格納されている値がユニークな存在でなければならない

statusカラム
presence: true => 値が必ず格納されていなければならない

enumについてですが、列挙型と呼ばれているもので、数値のカラムに対してプログラム上で別名を与えることができます。
statusinteger型が格納されていますが、「0がtodoのステータスで、1がdoingのステータスで、、、、」というように、人間にはstatusの数値が何を表すのか理解しづらいです。そこでDB上では数値で扱うが、プログラマから見たら文字列で扱うというように設定できるenumを使用するのです。詳しくはこちらを参照してください。

導入手順

ここから少し長いですが、1つずつ段階ごとに手順を紹介します。

RSpecとFactoryBotをインストール

RSpecは上述した通りテスティイングフレームワークです。
FactoryBotとはRSpecでテストする時にモデルのレコードを作成しテストで使用することができます。

インストール手順は公式のREADMEの手順を参照しています。
RSpecの公式README
FactoryBotの公式README

Gemfile:development グループと :testグループの両方に rspec-railsfactory_bot_railsを追加します。

Gemfile
group :development, :test do
  gem 'factory_bot_rails'
  gem 'rspec-rails', '~> 4.0.2'
  gem 'byebug', platforms: %i[mri mingw x64_mingw]
end

いつも通りbundle install

$ bundle install

それに加えて、$ rails generate rspec:installを実行

`rails generate rspec:install`
Running via Spring preloader in process 31987
      create  .rspec
      create  spec
      create  spec/spec_helper.rb
      create  spec/rails_helper.rb

これでインストールは完了です。

Task model specのファイルを作成

$ rails generate rspec:model task
Running via Spring preloader in process 32051
      create  spec/models/task_spec.rb
      invoke  factory_bot
      create    spec/factories/tasks.rb

念のためtask_spec.rbのテストが実行可能かを確認しましょう。

$ bundle exec rspec spec/models/task_spec.rb
*

Pending: (Failures listed here are expected and do not affect your suite's status)

  1) Task add some examples to (or delete) /Users/sakidendaiki/Downloads/RUNTEQ/tasks/option_tasks/sample_app_for_rspec/spec/models/task_spec.rb
     # Not yet implemented
     # ./spec/models/task_spec.rb:4


Finished in 0.0053 seconds (files took 1.6 seconds to load)
1 example, 0 failures, 1 pending

taskモデルとuserモデルのFactoryBotのテストデータを作成する

taskモデルのFactoryBotをファイルを作成します。
モデルに対してFactoryBotのファイルを作成する場合、$ rails g factory_bot:model モデル名と記述します。

今回はタスクモデルのテストですので、$ rails g factory_bot:model taskのコマンドを実行してみましょう。

$ bin/rails g factory_bot:model task
Running via Spring preloader in process 28632
   identical  spec/factories/tasks.rb

次にtaskFactoryBotを以下のように記述します。

spec/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

上記を定義することで、FactoryBot.create(:task)で上で定義されたtaskモデルを呼び出すことができます。
コンソールで試してみましょう。

$ FactoryBot.create(:task)
   (0.1ms)  SAVEPOINT active_record_1
  User Exists (0.7ms)  SELECT  1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ?  [["email", "user_1@example.com"], ["LIMIT", 1]]

 略

=> #<Task:0x00007f92a4342b08
 id: 1,
 title: "title_1",
 content: "content",
 status: "todo",
 deadline: Mon, 25 Jan 2021 07:24:22 UTC +00:00,
 created_at: Mon, 18 Jan 2021 07:24:22 UTC +00:00,
 updated_at: Mon, 18 Jan 2021 07:24:22 UTC +00:00,
 user_id: 1>

sequence(:title, "title_1")は一度FactoryBotを呼ばれると、titleカラムに対し"title_1"という文字列を格納します。
二度目に呼ばれる時は文字列は"title_2"となります。つまり、呼び出されるたびに末尾の数字に1を加えます。

何故このようなことをするかというと、titleカラムにはuniqueness: trueのバリデーションがあるためです。
titleカラムに値がかぶるのを防ぐために、呼び出されるたびにtitleカラムの値を変えています。
sequenceメソッドはこのようにuniqueness: trueがある時に使用されます。

deadlineカラムで定義されている1.week.from_nowは文字通りの意味です。
一週間後の同時刻の時間を出力します。
本当にRailsって理解しやすいですよね。

association :userは外部キー参照と非常に似ています。
taskモデルはuserモデルと1:多の関係にあるので、user_idカラムがあります。
association :userとすることで、FactoryBotのファイルで定義されているfactory :userのテストデータを参照します。

Taskにはuser_idも必要となることを踏まえてuserFactoryBotも作成する必要があります。

bin/rails g factory_bot:model user
Running via Spring preloader in process 29285
      create  spec/factories/users.rb

userモデルのFactoryBotを記述

spec/factories/users.rb
FactoryBot.define do
  factory :user do
    sequence(:email) { |n| "user_#{n}@example.com" }
    password { "password" }
    password_confirmation { "password" }
  end
end

sequenceメソッドの記載方法がtaskモデルで記載した時と違いますが、内容は同じです。
つまり、FactoryBotのファクトリである:userが呼び出されるたびに、nの値がどんどん増えていくことで、emailカラムの値がユニークとなるようにしています。

これでテストデータの準備は完了です!

--format documentation を追記(テスト記述前その1)

RSpecのテストを記述する前に、.rspecファイルに --format documentationを追記しましょう。

.rspec
--require spec_helper
--format documentation

これにより、テストの実行結果をドキュメント形式に変更します。
簡単にいうと、テスト結果が人間に読みやすくなります。

config.include FactoryBot::Syntax::Methodsを追記(テスト記述前その2)

テストを記載する際、テストデータの呼び出し方はFactoryBot.create(:task)や、FactoryBot.build(:task)といったように、必ずFactoryBotと記載しなければいけません。しかし、テストデータを呼び出す際に毎回書くのは冗長的です。
そこでconfig.include FactoryBot::Syntax::Methodsruby:spec/rails_helper.rbに記載します。

spec/rails_helper.rb
config.include FactoryBot::Syntax::Methods

こうすることで、テストデータを呼び出す時、create(:task)というように、FactoryBotの記述を省略できます。

task_spec.rbにモデルバリデーションに関するテストを作成

いよいよテストを記述します。

spec/models/task_spec.rb
require 'rails_helper'

RSpec.describe Task, type: :model do
  describe 'validation' do
    it '全ての属性が適切に格納されていれば有効' do
      task = build(:task)
      expect(task).to be_valid
      expect(task.errors).to be_empty
    end

    it 'タイトルがなければ無効' do
      task_without_title = build(:task, title: "")
      expect(task_without_title).to be_invalid
      expect(task_without_title.errors[:title]).to eq ["can't be blank"]
    end

    it 'ステータスがなければ無効' do
      task_without_status = build(:task, status: nil)
      expect(task_without_status).to be_invalid
      expect(task_without_status.errors[:status]).to eq ["can't be blank"]
    end

    it '同じタイトルが重複していれば無効' 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

    it 'タイトルが別名であれば有効' do
      task = create(:task)
      task_with_another_title = build(:task, title: 'another_title')
      expect(task_with_another_title).to be_valid
      expect(task_with_another_title.errors).to be_empty
    end
  end
end

it '〜〜〜〜〜〜' doの箇所にテストの内容が記載しています。
1つ目のテスト'全ての値が適切に格納されていれば有効'は、テストデータをそのまま呼び出しテストしています。
テストデータはバリデーションを考慮して値が定義されているので有効であるはずです。

expect(task).to be_validexpect(task.errors).to be_emptyは直感的に理解できるのではないでしょうか。
変数taskbe_valid(有効)であり、task.errorsbe_empty(空値)であることをテストしています。
もしバリデーションに引っかかっていたらtask.errorsには何かしらのエラーメッセージが格納されているはずです。

expect(task_without_title).to be_invalidexpect(task_without_title.errors[:title]).to eq ["can't be blank"]はどうでしょうか。簡単に推測できると思います。

テスト結果を確認

RSpecでテストします。
$ bundle exec rspecをターミナルに実行するだけです。

$ bundle exec rspec 

Task
  validation
    is valid with all attributes
    is invalid without title
    is invalid without status
    is invalid with a duplicate title
    is valid with another title

Finished in 0.14338 seconds (files took 1.39 seconds to load)
5 examples, 0 failures

バリデーションを削除してテストが失敗するか確認

本当にテストが正しいのかを確認するためにあえてテストを失敗させる方法があります。
例えば、2つ目のテスト'タイトルがなければ無効'が成功するのは、taskモデルのtitleカラムに対してpresence: trueのバリデーションがあるためです。では、presence: trueを削除してみましょう。

  validates :title, uniqueness: true

【テストの実行結果】

Failures:

  1) Task validation is invalid without title
     Failure/Error: expect(task.errors[:title]).to include("can't be blank")
       expected [] to include "can't be blank"
     # ./spec/models/task_spec.rb:12:in `block (3 levels) in <top (required)>'

Finished in 0.10893 seconds (files took 0.7813 seconds to load)
5 examples, 1 failure

Failed examples:

rspec ./spec/models/task_spec.rb:9 # Task validation is invalid without title

成功していたテストが失敗したことで、自分の書いたテストが正しいテストであったことを証明できるのです。