RSpecとは
Rubyにおけるテスティイングフレームワークのことです。
Rubyでは標準でMinitest
という別のフレームワークがあります。Ruby on RailsチュートリアルでもMinitest
でテスト方法を教わります。
ですが、実際に現場で使われているテスティイングフレームワークはRSpec
が圧倒的に多いようです。
本記事ではRSpec
をインストールし、既に定義されているモデルに対してバリデーションテストするまでを記載します。
使用技術のバージョン
・Ruby: 2.6.4
・Ruby on Rails: 5.2.3
モデルの概要
今回取り扱うモデルはuser
モデルとtask
モデルです。
[userモデル]
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モデル]
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
についてですが、列挙型と呼ばれているもので、数値のカラムに対してプログラム上で別名を与えることができます。
status
はinteger
型が格納されていますが、「0がtodoのステータスで、1がdoingのステータスで、、、、」というように、人間にはstatus
の数値が何を表すのか理解しづらいです。そこでDB上では数値で扱うが、プログラマから見たら文字列で扱うというように設定できるenum
を使用するのです。詳しくはこちらを参照してください。
導入手順
ここから少し長いですが、1つずつ段階ごとに手順を紹介します。
RSpecとFactoryBotをインストール
RSpec
は上述した通りテスティイングフレームワークです。
FactoryBot
とはRSpec
でテストする時にモデルのレコードを作成しテストで使用することができます。
インストール手順は公式のREADMEの手順を参照しています。
RSpecの公式README
FactoryBotの公式README
Gemfile
の :development
グループと :test
グループの両方に rspec-rails
とfactory_bot_rails
を追加します。
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
次にtask
のFactoryBot
を以下のように記述します。
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
も必要となることを踏まえてuser
のFactoryBot
も作成する必要があります。
bin/rails g factory_bot:model user Running via Spring preloader in process 29285 create spec/factories/users.rb
user
モデルのFactoryBot
を記述
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
を追記しましょう。
--require spec_helper --format documentation
これにより、テストの実行結果をドキュメント形式に変更します。
簡単にいうと、テスト結果が人間に読みやすくなります。
config.include FactoryBot::Syntax::Methodsを追記(テスト記述前その2)
テストを記載する際、テストデータの呼び出し方はFactoryBot.create(:task)
や、FactoryBot.build(:task)
といったように、必ずFactoryBot
と記載しなければいけません。しかし、テストデータを呼び出す際に毎回書くのは冗長的です。
そこでconfig.include FactoryBot::Syntax::Methods
をruby:spec/rails_helper.rb
に記載します。
config.include FactoryBot::Syntax::Methods
こうすることで、テストデータを呼び出す時、create(:task)
というように、FactoryBot
の記述を省略できます。
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_valid
とexpect(task.errors).to be_empty
は直感的に理解できるのではないでしょうか。
変数task
はbe_valid
(有効)であり、task.errors
はbe_empty
(空値)であることをテストしています。
もしバリデーションに引っかかっていたらtask.errors
には何かしらのエラーメッセージが格納されているはずです。
expect(task_without_title).to be_invalid
とexpect(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
成功していたテストが失敗したことで、自分の書いたテストが正しいテストであったことを証明できるのです。