○概要
一つのロジックを変えると、100個くらいのspecエラーが発生することも有ります。
specの消化活動をしていくうちに、継ぎ接ぎだらけのテストになってしまう場合もあります。
そこで、今回は継ぎ接ぎだらけのRSpecにしないための統一ルールをまとめました。
(経験も浅いので、どんどん更新していきます。)
○理想的なテスト
テストの理想 = MECE + Readable + Flexible
MECE
テストの対象に対して、必要十分なテストケースを用意するべきです。
Readable
他のspecファイルと違う変数名を入れないようにしましょう。
短くて適当よりも、長くて正確のほうが良いです。
Flexible
テストデータが深くネストをしていたり、
before :eachでくくりすぎという問題が、specの修正を困難にします。
○命名規則
「AAメソッドは、BBの時、CCをDDします。」
AA : Describe
コントローラーのメソッド名を表記
ex)
describe "GET #show"
BB : Context
- 状況を表記
- when/withで始める
ex)
context "when book is present"
context "with valid params"
CC : Subject
- テスト対象を表記
ex)
# eqの場合
subject{ assings(:book) }
subject{ Book.find(1)}
# change/render/redirect/http statusの場合
subject {Proc.new { get: index }}
subject {Proc.new { get :show, id: 1 }}
subject {Proc.new { get :new, id: 1 }}
subject {Proc.new { get :edit, id: 1 }}
subject {Proc.new { post :create, id: 1, book: @valid_params }}
subject {Proc.new { patch :update, id: 1, book: @valid_params }}
subject {Proc.new { destroy :delete, id: 1 }}
Dd : it
- テスト対象に対する動詞を表記
ex)
# eqの場合
it "主語 is 目的語"
it "主語 has 目的語"
# changeの場合
it "create 目的語"
it "update 目的語"
it "delete 目的語"
# renderの場合
it "render the template名"
# redirectの場合
it "redirect to path名"
# http statusの場合
it "returns http ok"
it "returns http success"
it "returns http 204"
○設計規則
①describe > context > subject > it
subjectとshared_exampleを使うとかなりdryになるし、柔軟性も高い。
②基本的にbefore :eachを使用しない。
testは独立していたほうが、修正しやすいし読みやすい。
let! と letを使い分けたほうが操作性が楽。
let!も多様しない。テストデータを修正するときに影響範囲が大きい
○マッチャand命名規則
基本的には、以下の4つを各controllerのmethod毎に使用します。
- eq系(変数取得)
- change系(モデルの増減)
- render/redirects系
- http status系
以上の4種類を使います。
* eq系(変数取得)
[概要]
値の整合性を確認します。
[マッチャ例]
expect( subject ).to eq @book
expect( subject ).not_to eq @book
* change系(モデルの増減)
[概要]
モデルのレコードの増減を確認します。
目的語は正確に書きます。
なるべく、change.from().to()を使いましょう。
[マッチャ例]
増
expect{ subject.call }.to change(Book, :count).from(0).to(1)
expect{ subject.call }.to change(Book, :count).by(1)
減
expect{ subject.call }.to change(Book, :count).from(1).to(0)
expect{ subject.call }.to change(Book, :count).by(-1)
不変
expect{ subject.call }.to change(Book, :count).from(0).to(0)
expect{ subject.call }.to change(Book, :count).by(0)
* render/redirect系
[概要]
render template/ redirect_to を確認します。
[マッチャ例]
render_template
subject.call
expect(response).to render_template :show
redirect_to
subject.call
expect(response).to redirect_to(book_path)
* http status系
[概要]
http statusが正常かどうかを確認します。
[マッチャ例]
status code == 200 の場合
subject.call
expect(response).to have_http_status(:ok)
status code ==2xx の場合
subject.call
expect(response).to have_http_status(:success)
status code == 204 の場合
subject.call
expect(response).to have_http_status(204)
他にも書き方はありますが、紛らわしいので上で統一します。
○トラブルシューティング
インデントに気をつける。
事実かどうかは怪しいが、インデントが揃ってないとdbクリーナーが正常に働かない事がある。
全体ファイルでテストした時に、エラーが出る可能性がある。
letの変数と、ローカル変数の名前を一緒にしない。
競合してると、エラーが出る可能性がある。
単体ファイルよりも、全体ファイルでテスト回すとエラーが出る可能性がある。
○RSpecベストプラクティス
require "rails_helper"
describe BooksController do
# http stasus case
shared_examples_for "returns http success" do
it { subject.call; expect(response).to have_http_status(:success)}
end
# render template case
shared_examples_for "render template" do |template|
it { subject.call; expect(response).to render_template template}
end
# redirect to path case
shared_examples_for "redirect to path" do |path|
it { subject.call; expect(response).to render_to path}
end
# create/update/delete model case
shared_examples_for "create Book" do |model|
it { expect{subject.call}.to change{model.count}.by(1) }
end
shared_examples_for "update Model" do |model|
it { expect{subject.call}.to change{model.count}.by(0) }
end
shared_examples_for "delete Model" do |model|
it { expect{subject.call}.to change{model.count}.by(-1) }
end
# eq case
shared_examples_for "assinged value is @value" do |value|
subject.call
it { expect(value).to eq value }
end
let(:author) { create(:author)}
let(:book) { create(:book, author: author) }
let(:valid_params) { attributes_for(:book, author: author) }
let(:invalid_params) { attributes_for(:book, author: nil) }
describe "GET #show" do
subject {Proc.new { get :show, id: 1 }}
before { @value = book}
it_behaves_like "returns http success"
it_behaves_like "assinged value is value" :book
end
describe "POST #create" do
context "with valid params" do
subject {Proc.new { post :create, id: 1, book: valid_params }}
it_behaves_like "returns http success"
it_behaves_like "create Model" Book
it_behaves_like "redirect to path" root_path
end
context "with invalid params" do
subject {Proc.new { post :create, id: 1, book: invalid_params }}
it ~~~~
# ここも同じ要領でshared_exampleで作成しても良いですし、直接書いてもいいです。
end
end