はじめに
こんにちは!先日mira meet(弊社のミートアップ)で発表させていただいたRSpecのShoulda Matchersがめちゃくちゃ便利なのでこちらでもご紹介させていただきます。
Shoulda Matchersとは
Shoulda MatchersとはRSpec内で使用できるライブラリで、複雑なテストをワンライナーで書くことができるマッチャのことです。
通常のテストの場合
例えば、下記のようなUserモデルがあるとします。
バリデーションテストをする場合、下記のような観点が必要だとします。
・ nameがnilの場合、無効であること ・ nameが空文字の場合、無効であること ・ nameが既に保存されている場合、無効であること ・ nameが10文字以内の場合、有効であること ・ nameが11文字以上の場合、無効であること ・ emailがnilの場合、無効であること ・ emailが空文字の場合、無効であること ・ emailが既に保存されている場合、無効であること ・ emailがemailの形式ではない場合、無効な状態であること ・ emailは全角文字を使用する場合、無効な状態であること ・ passwordがnilの場合、無効であること ・ passwordが空文字の場合、無効であること ・ passwordが5文字以内の場合、無効であること ・ passwordが6文字以上の場合、有効であること ・ passwordが128文字以内の場合、有効であること ・ passwordが129文字以上の場合、無効であること ・ password_confirmationがnilの場合、無効であること ・ password_confirmationが空文字の場合、無効であること ・ passwordとpassword_confirmationが不一致の場合、無効であること
この時点で膨大ですが、アプリケーションが大きくなればなるほど必要な観点は多くなります。これをテストに起こすとなると非常に大変です。
では、通常通りRSpecを使用してテストを書いてみましょう。nameを例に書いてみると下記のようになります。
require 'rails_helper'
RSpec.describe User, type: :model do
describe 'name' do
before do
@user = User.new(
name: "Tanaka",
email: 'tester@example.com',
password: 'p@ssword!!',
password_confirmation: 'p@ssword!!',
)
end
it 'nilの場合、無効であること' do
@user.name = nil
@user.valid?
expect(@user.errors[:name]).to include("can't be blank")
end
it '空文字の場合、無効であること' do
@user.name = ""
@user.valid?
expect(@user.errors[:name]).to include("can't be blank")
end
it 'すでに使用されているnameの場合、保存できないこと' do
User.create(
name: 'Takashi',
email: 'tester_1@example.com',
password: 'p@ssword!!',
password_confirmation: 'p@ssword!!',
)
@user.name = 'Takashi'
@user.valid?
expect(@user.errors[:name]).to include("has already been taken")
end
it '10文字以内の場合、有効であること' do
@user.name = 'TakashiKai'
expect(@user).to be_valid
end
it '11文字以上の場合、無効であること' do
@user.name = 'TakashiKaii'
@user.valid?
expect(@user.errors[:name]).to include("is too long (maximum is 10 characters)")
end
end
end
少し冗長ですね。特にnameの値がない場合のテストでは、空文字、nullどちらもテストが必要ですし、文字数も境界線になる10文字と11文字でテストしないといけないのが辛いところです。
Shoulda Matchersを使用したテストの場合
ではShoulda Matchersを使用してテストを実装してみましょう。
require 'rails_helper'
RSpec.describe User, type: :model do
describe 'name' do
it { is_expected.to validate_presence_of :name }
it { is_expected.to validate_uniqueness_of :name }
it { is_expected.to validate_length_of(:name).is_at_most(10) }
end
end
!?
なんと実際のテスト実行部分が3行になってしまいました!
・ nameがnilの場合、無効であること
・ nameが空文字の場合、無効であること
こちらの観点は it { is_expected.to validate_presence_of :name }
という一文でテストがされています。もしわかりづらければ、下記のように書き換えて実行してみてください。
require 'rails_helper'
RSpec.describe User, type: :model do
describe 'name' do
user = User.new(
name: "", # またはname: nil
email: 'tester@example.com',
password: 'p@ssword!!',
password_confirmation: 'p@ssword!!',
)
it { expect(user).to validate_presence_of :name }
end
end
userインスタンスのnameの値を空文字にしてもnilにしてもテストが通ることが確認できると思います。
・ nameが既に保存されている場合、無効であること
こちらの観点は it { is_expected.to validate_uniqueness_of :name }
の一文でテストされています。
・ nameが10文字以内の場合、有効であること
・ nameが11文字以上の場合、無効であること
こちらの観点はit { is_expected.to validate_length_of(:name).is_at_most(10) }
の一文でテストされています。こちらの10という数字を9や11にしてテストしてみてください。例えば9にした時は下記のようなエラーが出ます。
Failures:
1) User name is expected to validate that the length of :name is at most 9
Failure/Error: it { is_expected.to validate_length_of(:name).is_at_most(9) }
Expected User to validate that the length of :name is at most 9, but
this could not be proved.
After setting :name to ‹"xxxxxxxxxx"›, the matcher expected the
User to be invalid, but it was valid instead.
# ./spec/models/user_spec.rb:5:in `block (3 levels) in <top (required)>'
Finished in 0.47804 seconds (files took 3.31 seconds to load)
1 example, 1 failure
9文字が最大だから‹"xxxxxxxxxx"›
のように10文字で保存しようとすると無効になることを期待したが、有効になっているとのことです。つまり、9文字なら有効、だけでなく10文字なら無効というテストも行っていることがわかります。
このようにShoulda Matchersを使用すると非常に簡単に簡潔にテストを記述することができるようになります。また、今回はモデルを例にご紹介しましたが、ActiveRecordやController、Routingでも使用できるマッチャが揃っています。興味がある方はぜひGitHubを覗いてみてください。
まとめ
今回はShoulda Matchersをご紹介しました。テストはアプリケーションが大きくなればなるほど管理が難しく、冗長になりやすい傾向があります。しかし、このマッチャを使用すれば、簡素に書くことができますし、エラー内容もわかりやすいなどのメリットもあるため、ぜひ利用をお勧めします。
では!よいテストコードライフを!!