160
144

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

RSpecを綺麗に書くための基本Rule

Last updated at Posted at 2015-06-27

○概要

一つのロジックを変えると、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

160
144
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
160
144

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?