先日RSpecのテストコードにレビューを頂いたので、
なるべく多くを学びたいと思い、その内容を咀嚼する過程での気付きを書きたいと思います
元記事
RSpecのEmail一意性テストで"Email has already been taken"問題と回避策 - Qiita
-
なるべく小さいscope?(contextの中など)を見ただけでテストの内容が把握できるような書き方のほうが、読み手に理解しやすい、想定外の挙動を防ぐことができる。
-
let
を使って書くことはこれを実現するために有効 -
let
の遅延評価によって、テストによっては必要のないインスタンス変数を毎回before
で定義するような冗長性を回避できる
が、この遅延評価を理解せずに、なんとなく変数代入の感覚で使用していたために以下のようなエラーに遭遇しました
let
を使用することで、通るはずのテストが通らないfalse Alarmを生じる可能性がある
let
とlet!
の使い分けが重要になりそうなケース
- テストを実施する時点と、前提となる条件の間に時間的なずれがある場合
- すでに完了しているものと、現在の比較が必要な場合
- モデルのテストの場合なら、一意性のテスト
遅延評価されるletが、実際にテストの失敗を招いたケース
レビューの内容を参考に、
属性の一意性についてのテストを書いてみた。
User modelの属性username
の一意性についてのテストを以下のうように書くと
describe "username" do
...
context "usernameが重複している場合" do
let(:existing_user) { FactoryBot.create(:user, username: "alice") }
subject { FactoryBot.build(:user, username: "alice" ) }
it { is_expected.to be_invalid }
end
...
end
モデルでusername
属性についてのvalidationが記述されているにもかかわらず
class User < ApplicationRecord
validates :username, presence: true, uniqueness: true, length: { maximum: 12 }
...
end
validationがinvalidにならない
1) User#create username usernameが重複している場合 is expected to be invalid
Failure/Error: it { is_expected.to be_invalid }
expected `#<User id: nil, email: "test_user_5@example.com", created_at: nil, updated_at: nil, username: "alice">.invalid?` to return true, got false
テストの記述を変更する
(実際にレビューコードではこの書き方を提示してくれていました)
let
>> let!
describe "username" do
...
context "usernameが重複している場合" do
let!(:existing_user) { FactoryBot.create(:user, username: "alice") } #let!に変更
subject { FactoryBot.build(:user, username: "alice" ) }
it { is_expected.to be_invalid }
end
...
end
このようにすると、想定通りテストがパスするようになる。
let
とlet!
の違い
- letは、itやexampleが実行されるまで評価されない
- let!は即座に実行される
it { is_expected.to be_invalid }
の時点で、existing_user
は存在完了していないといけない
英文でいうところlet
は現在完了的な振る舞いをして、let!
は過去完了の状態をつくってくれると考えると個人的にはしっくりきた。