RSpec - 'users validation'のテストコードをレビューしてもらった結果
@jnchito さんにレビューしていただきました!
よければ上のリンクからどうぞ!
経緯
初めまして。Qiitaの投稿、rails、rspecの初心者です。お手柔らかにお願いします。
railstutorial で学習しながら、app/models/user.rb
の'model spec'を書いています。
イケてる綺麗なテストコードにしたいと思い、色々な方のrspecの書き方の記事を拝見させてもらいました。
specを自分なりに工夫しながら書いていて、この書き方「結構イケてるのでは?」と思う反面、
いざ記事を参考にしながら自分のコードを見返していると何か微妙に感じてきてしまい、
「ルールに従ったテストコードがなんなのか」と迷子になり、
なかなかcommitできず、地獄を彷徨っています。
もしよければ、テストコードのレビュー、指摘をしていただけると幸いです。
参考
テストコードは、主に jnchito さんの以下を参考にさせてもらいました。
上記を拝見させて頂いて自分なりに工夫して書いたコードになります。
以下は参考にしたわけではないですが、拝見して自分のコードがゴミに思えた記事。
ネストイメージ
- describe:
User(model)
の#create(action)
のときattribute: name
は、 - context:
name should be present
名前が存在するべき。 - it:
valid
orinvalid
のパターンを検証。
テスト作成において、ネストのイメージ(?)ですが、
model
> action
> attribute
> validates: presence:true
> valid or invalid
のような感じでグループ化を意識してspecを書きました。
(もう一点イメージとして、記事の最下部の$ bundle exec rspec
の実行結果が綺麗に見えるようにといった感じです。)
地獄から抜け出せない理由
-
describe
、context
、it
の正しい使い分け。
工夫して他の人が読む場合を考慮してイケてるわかりやすく書いたつもりですが、
RSpecを綺麗に書くための基本Rule を拝見して、
「これじゃダメだ。」となった。
(そもそもルールがあるのでしょうか?ある程度のルールを自分の中で作って作成する認識でいるのでそれが問題かもしれません。) - プロから見た僕のテストコード
書いたテストコードがグリーンで「問題ない」と思ってしまっているので、
「俺ならこうする!」が知りたいです。 - いちいち、「保存できることorできないこと」は、各テストケースで実施しなくても良いものなのか。
基本的にはuser.valid?
として、確実に対象のテストで落ちていることを保証するために、
エラー(errors[:name]
)の中身をinclude("can't be blank")
のように確認していますが、
その後、user.save!
として、、(!
これは例外で落ちてしまう気がしてますが、、)
以下を確認するべきなのかを迷っています。-
valid
のケースは、保存できること -
invalid
のケースは、保存できないこと
-
以下が「保存できることorできないこと」を確認しているコードの抜粋です。
# nameが存在していない場合は、無効であること
it 'is invalid because the name value does not exists' do
user.name = nil
user.valid?
expect(user.errors[:name]).to include("can't be blank")
expect(user.save).to be_falsey # ...この1行で保存できないことを確認
end
テストコード
テストの内容は、よくあるusers.name
, user.email
に関するバリデーションです。
railstutorial いうところの こちら になります。
では、早速ですが、、
以下が現状、掛かっているバリデーション。
class User < ApplicationRecord
before_save :downcase_email
validates :name,
presence: true,
length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email,
presence: true,
length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
private
def downcase_email
self.email.downcase!
end
end
以下がFactoryBot
で定義しているuser
。
FactoryBot.define do
factory :user do
name { "hoge" }
email { "foo@bar.com" }
end
end
以下が実際のテストコード。
require 'rails_helper'
require 'pry'
RSpec.describe User, type: :model do
let(:user) { FactoryBot.build(:user) }
describe '#create' do
describe 'attribute: name' do
context 'name should be present' do
# nameが存在している場合は、有効であること
it 'is valid because the name has the correct value' do
expect(user).to be_valid
expect(user.save).to be_truthy
end
# nameが存在していない場合は、無効であること
it 'is invalid because the name value does not exists' do
user.name = nil
user.valid?
expect(user.errors[:name]).to include("can't be blank")
expect(user.save).to be_falsey
end
end
context 'name should be length is 50 characters or less' do
# nameが50文字以下の場合は、有効であること
it 'is valid because the name length is 50 characters or less' do
user.name = 'a' * 50
user.valid?
expect(user.errors[:name]).not_to include('is too long (maximum is 50 characters)')
expect(user.save).to be_truthy
end
# nameが50文字を超える場合は、無効であること
it 'is invalid because the name length is 50 characters over' do
user.name = 'a' * 51
user.valid?
expect(user.errors[:name]).to include('is too long (maximum is 50 characters)')
expect(user.save).to be_falsey
end
end
context 'name should be not blank' do
# nameが空白の場合は、無効であること
it 'is invalid because the name is blank' do
user.name = ' '
user.valid?
expect(user.errors[:name]).to include("can't be blank")
expect(user.save).to be_falsey
end
end
end
context 'attribute: email' do
context 'email should be present' do
# emailが存在していない場合は、無効であること
it 'is invalid because the email value does not exists' do
user.email = nil
user.valid?
expect(user.errors[:email]).to include("can't be blank")
expect(user.save).to be_falsey
end
end
context 'email should be length is 50 characters or less' do
# emailが255文字以下の場合は、有効であること
it 'is valid because the email length is 255 characters or less' do
user.email = 'a' * 243 + '@example.com'
user.valid?
expect(user.errors[:email]).not_to include('is too long (maximum is 255 characters)')
expect(user.save).to be_truthy
end
# emailが255文字を超える場合は、無効であること
it 'is invalid because the email length is 255 characters over' do
user.email = 'a' * 244 + '@example.com'
user.valid?
expect(user.errors[:email]).to include('is too long (maximum is 255 characters)')
expect(user.save).to be_falsey
end
end
context 'email should be correct format' do
# emailの形式が正しい場合は、有効であること
it 'is valid because the email is the correct format' do
user.email = 'user@example.com'
expect(user).to be_valid
expect(user.save).to be_truthy
user.email = 'USER@foo.COM'
expect(user).to be_valid
expect(user.save).to be_truthy
user.email = 'A_US-ER@foo.bar.org'
expect(user).to be_valid
expect(user.save).to be_truthy
user.email = 'first.last@foo.jp'
expect(user).to be_valid
expect(user.save).to be_truthy
user.email = 'alice+bob@baz.cn'
expect(user).to be_valid
expect(user.save).to be_truthy
end
# emailの形式が正しくない場合は、無効であること
it 'is invalid because the email format is incorrect' do
user.email = 'user@example,com'
expect(user).not_to be_valid
expect(user.save).to be_falsey
user.email = 'user_at_foo.org'
expect(user).not_to be_valid
expect(user.save).to be_falsey
user.email = 'user.name@example.'
expect(user).not_to be_valid
expect(user.save).to be_falsey
user.email = 'foo@bar_baz.com'
expect(user).not_to be_valid
expect(user.save).to be_falsey
user.email = 'foo@bar+baz.com'
expect(user).not_to be_valid
expect(user.save).to be_falsey
end
end
context 'email should be unique' do
# 同一のemailが既に登録されている場合は、無効であること
it 'is Invalid because the same email already exists' do
dup_user = user.dup
dup_user.email = dup_user.email.upcase
dup_user.save!
user.valid?
expect(user.errors[:email]).to include('has already been taken')
expect(user.save).to be_falsey
end
end
# emailが小文字で保存されていること
context 'email should be saved in lowercase' do
it 'is valid because the email saved in lowercase' do
user.email = 'Foo@Example.Com'
user.save!
expect(user.reload.email).to eq 'foo@example.com'
end
end
context 'email should be not blank' do
# emailが空白の場合は、無効であること
it 'is invalid because the email is blank' do
user.email = ' '
user.valid?
expect(user.errors[:email]).to include("can't be blank")
expect(user.save).to be_falsey
end
end
end
end
end
上記のテストは、$ bundle exec rspec
を実行(グリーン)した出力は以下。
テストコードのdescribe
、context
、it
の使い分けは、以下の出力が見やすくなるようにを意識してネストした感じなります。
User
#create
attribute: name
name should be present
is valid because the name has the correct value
is invalid because the name value does not exists
name should be length is 50 characters or less
is valid because the name length is 50 characters or less
is invalid because the name length is 50 characters over
name should be not blank
is invalid because the name is blank
attribute: email
email should be present
is invalid because the email value does not exists
email should be length is 50 characters or less
is valid because the email length is 255 characters or less
is invalid because the email length is 255 characters over
email should be correct format
is valid because the email is the correct format
is invalid because the email format is incorrect
email should be unique
is Invalid because the same email already exists
email should be saved in lowercase
is valid because the email saved in lowercase
email should be not blank
is invalid because the email is blank
どうぞ、宜しくお願いします。