woshidan's blog

あいとゆうきとITと、とっておきの話。

いまさらRSpecに少し慣れてテストをすっきり書く方法について少し覚えた話

いまさらRSpecをさわってきて慣れて覚えた話もメモしておきます。

TL;DR

  • letで宣言した変数の初期化用パラメータで置き換えたくなったパラメータもletで宣言しておくとよさそう
  • shared_context や shared_examplesでまとめるのがちょうどいいくらいの事例としてちょうどいいのは、外部公開するAPIの異常系のレスポンスのテストくらいでは

慣れてきて覚えた話

letで宣言した変数の初期化用パラメータで置き換えたくなったパラメータもletで宣言しておく

これは同僚の方から教えていただいていいな、と思ったんですが、

describe "post comment" do
  let(:params) { { user_name: "HOGE", comment: "...", "book_id": 3 } }

  context "valid request" do
    it "can be posted" do
      ...
    end
  end

  context "invalid user" do
    let(:params) { { user_name: "", comment: "...", "book_id": 3 } }

    it "cannot be posted" do
      ...
    end
  end

  context "invalid comment" do
    let(:params) { { user_name: "HOGE", comment: "INVALID_FORMAT", "book_id": 3 } }

    it "cannot be posted" do
      ...
    end
  end

  context "invalid book" do
    let(:params) { { user_name: "HOGE", comment: "...", "book_id": nil } }

    it "cannot be posted" do
      ...
    end
  end
end

のような場合、 params の値の一部がそれぞれの context で異なるので、それぞれの context ブロックでパラメータを宣言しなおしていますが、

describe "post comment" do
  let(:user_name) { "HOGE" }
  let(:comment) { "..." }
  let(:book_id) { 3 }
  let(:params) { { user_name: "HOGE", comment: "...", "book_id": 3 } }

  context "valid request" do
    it "can be posted" do
      ...
    end
  end

  context "invalid user" do
    let(:user_name) { "" }

    it "cannot be posted" do
      ...
    end
  end

  context "invalid comment" do
    let(:comment) { "INVALID_FORMAT" }

    it "cannot be posted" do
      ...
    end
  end

  context "invalid book" do
    let(:book_id) { nil }

    it "cannot be posted" do
      ...
    end
  end
end

のように、example内で触る変数はparamsだけかもしれないんですが、それぞれのcontextで注目しているパラメータだけあとで置き換えられるように、宣言の時に工夫しておくとすっきりするなと思いました。

shared_context や shared_examplesでまとめるのがちょうどいいくらいの事例としての外部公開するAPIの異常系のレスポンスのテスト

この話がしたくてこの記事を書きはじめたのに随分長くなってしまったのですが、RSpecでは、shared_contextを使ってspecの中で繰り返し登場するテストケースを

describe "post comment" do
  let(:user_name) { "HOGE" }
  let(:comment) { "..." }
  let(:book_id) { 3 }
  let(:params) { { user_name: "HOGE", comment: "...", "book_id": 3 } }

  context "valid request" do
    it "can be posted" do
      ...
    end
  end

  context "invalid user" do
    let(:user_name) { "" }

    it "cannot be posted" do
      ...
      expect(:status).to eq 400
      expect(:body).to eq "Invalid Request" # このケース自体がいいかは微妙ですが...
    end
  end

  context "invalid comment" do
    let(:comment) { "INVALID_FORMAT" }

    it "cannot be posted" do
      ...
      expect(:status).to eq 400
      expect(:body).to eq "Invalid Request"
    end
  end

  context "invalid book" do
    let(:book_id) { nil }

    it "cannot be posted" do
      ...
      expect(:status).to eq 400
      expect(:body).to eq "Invalid Request"
    end
  end
end

のような場合、

describe "post comment" do
  let(:user_name) { "HOGE" }
  let(:comment) { "..." }
  let(:book_id) { 3 }
  let(:params) { { user_name: "HOGE", comment: "...", "book_id": 3 } }

  context "valid request" do
    it "can be posted" do
      ...
    end
  end

  shared_examples "invalid request" do
    it "returns 400 response" do 
      expect(:status).to eq 400
      expect(:body).to eq "Invalid Request"
    end
  end

  context "invalid user" do
    let(:user_name) { "" }

    it "cannot be posted" do
      ...
      it_behaves_like "invalid request"
    end
  end

  context "invalid comment" do
    let(:comment) { "INVALID_FORMAT" }

    it "cannot be posted" do
      ...
      it_behaves_like "invalid request"
    end
  end

  context "invalid book" do
    let(:book_id) { nil }

    it "cannot be posted" do
      ...
      it_behaves_like "invalid request"
    end
  end
end

のように、同じテストコードを実行している部分を shared_examples に定義して it_behaves_like "SHARED_EXAMPLE_NAME" で呼び出すことができます。

こうするとテストケースはDRYになりますが、

  • 一方でテストが読みにくくなったり
  • バリデーションエラーなどは案外全く同じコードにならなかったり
    • たとえば、上記のサンプルコードは実際はパラメータごとに invalid user などのレスポンスを返したほうが親切でしょう

して、使いどころが難しいんですが、外部公開しているAPIの場合、

  • エラーの内容からAPI内部の処理を推測できないようにわざと詳細なレスポンスを返さないケースがある
  • 外部公開しているAPIが複数あるとしても、機能ごとにエラー時のレスポンスを変えない場合が多い
    • たとえば、一覧取得用API投稿用APIで認証エラー時のレスポンス内容は変えない方が親切でしょう

という感じで使ってみるとよいかもしれないな、と思ったのでメモです。

参考

https://qiita.com/jnchito/items/42193d066bd61c740612 https://qiita.com/etet-etet/items/7babe4856a1cd62b9ecb