letとlet!の使い分け(Ruby/Rspec)

プログラム

letとlet!の使い方ではまったので備忘録として残します。

letとは

letはインスタンス変数やローカル変数をletという機能で置き換えることができます。

ex)
@user = user.create
let(:user) { create(:user) }

letを使うと何が嬉しいかというと、テストコードがすっきりするというのと
letはその変数が必要になるまでは呼び出されないという遅延評価されるという特徴があるため、効率のよいテストコードを作成できることです。
beforeを使うとブロックの宣言時に全ての変数が評価されるため、
使用していない変数があったとしても評価されてしまい
あまり効率のよくないテストコードになってしまうことがあります。

let!は基本的にはbeforeと同じ意味になります。
つまりテストを実行する上で前提条件にあたる部分はletではなくlet!で記載してあげる必要があります。

ex)
before do
@user = user.create
end
let!(:user) { create(:user) }

ただしメソッドを事前に実行したい場合は、letでは記載できないため、beforeを使う必要があります。


before do
  sign_in(current_user)
end

はまったポイント

ぼくがはまったのは記事アプリを作成する際に
ログアウトの機能のテストコードを作成していたときです。

ぼくが最初に作成したテストコードは以下になります。
この場合、次のように処理されるためletで定義していても問題ありませんでした。
subjectが実行される
subjectの中でheaders使用されているため
let(:headers) { user.create_new_auth_token }が実行される
headersの中でuserが使用されているため
let(:user) { create(:user) }が実行される


describe "DELETE /api/v1/auth/sign_out" do
 subject { delete(destroy_api_v1_user_session_path, headers: headers) }
 context "ユーザーがログインしているとき" do
  let(:user) { create(:user) }
  let(:headers) { user.create_new_auth_token }
  it "ログアウトできる" do
   subject
   expect(response).to have_http_status(:ok)
  end
 end
end

次にヘッダーのトークン情報の変化を確認するために
以下のコードに修正しました。


describe "DELETE /api/v1/auth/sign_out" do
 subject { delete(destroy_api_v1_user_session_path, headers: headers) }
 context "ユーザーがログインしているとき" do
 let(:user) { create(:user) }
 let(:headers) { user.create_new_auth_token }
  it "ログアウトできる" do
   expect { subject }.to change { user.reload.tokens }.from(be_present).to(be_blank)
   expect(response).to have_http_status(:ok)
  end
 end
end
---エラー内容
Failure/Error:expect{subject}.to change{user.reload.tokens}.from(be_present).to(be_blank)
expected user.reload.tokens to have initially been be present, but was {}

expect { A }.to change { B }.from(X).to(Y)
とすると、ぼくは最初Aを実行後Bの前後でXからYに変化しているかのテストをしていると考えていました。
その場合、順番に処理していけばletで定義していても問題ないのでは?と考えていました。

メンターの方からの指摘で、正しくは
expect { A }.to change { B }.from(X).to(Y)
とすると、Aが発生した時にBがXからYに変化しているかのテストがやりたかったことになります。
つまりリロード(B)前後じゃなくsubject(A)の前後でuser.reload.tokensの値が変化しているかどうかをチェックする。

修正したコードです。
headersをlet→let!で定義するように修正しました。


describe "DELETE /api/v1/auth/sign_out" do
 subject { delete(destroy_api_v1_user_session_path, headers: headers) }
 context "ユーザーがログインしているとき" do
 let(:user) { create(:user) }
 let!(:headers) { user.create_new_auth_token }
  it "ログアウトできる" do
   expect { subject }.to change { user.reload.tokens }.from(be_present).to(be_blank)
   expect(response).to have_http_status(:ok)
  end
 end
end

今回はまったのはテストコードの処理を正しく理解していなかったため、
前提条件を正しく定義できていなかったことが原因でした。