読みやすいRSpec書いていますか?
読みやすいRSpecって何?
「簡潔でテストケースを見てテスト内容が理解しやすいもの」と考えています
ここでのテストケースとは、it/exampleブロックのことを指しています
例題
例えば、Rails製REST APIのRequest Specを書くことを考えてみます
コードは正しいか確認していないので、多少間違っていてもお許しください🙇
前提
- APIはたくさんある
- 良くないけど、クエリパラメータのチェックをcontrollerでしているため、テストはRequest Specで行う
APIの共通仕様
- Server Errorを除き、HTTPステータスコードは必ず200で返す
- HTTPレスポンスボディはJSON形式で返す
- 成功時のレスポンス
{ "status": 200, "message": "Succeed", "data": ... }
- 未認証時のレスポンス
{ "status": 401, "message": "Unauthorized" }
- リクエストパラメータが不正な時のレスポンス
{ "status": 400, "message": "Bad Request", "errors": ["error", ...] }
- 成功時のレスポンス
ユーザー一覧APIのSpec
/users?page=1
のようにアクセスすると、ユーザー一覧を返すAPIのテストを書いていきます
まず、APIへのリクエストを書いて
before do
get "/users", params: { page: page }
end
let(:page) { 1 }
単純に書くと
it "respond with a 401 response" do
expect(response).to have_http_status 200
expect(response.content_type).to eq "application/json"
expect(response.body).to be_json include(
"status" => 401,
"message" => "Unauthorized",
)
end
context "when logged in" do
include_context "logged in" # ログイン状態にするshared_contextを定義済み
context "when page is 1" do
let(:page) { 1 }
it "respond with a 200 response" do
expect(response).to have_http_status 200
expect(response.content_type).to eq "application/json"
expect(response.body).to be_json include(
"status" => 200,
"message" => "Succeed",
"data" => ..., # 省略
)
end
end
context "when page is 'a'" do
let(:page) { "a" }
it "respond with a 400 response" do
expect(response).to have_http_status 200
expect(response.content_type).to eq "application/json"
expect(response.body).to be_json include(
"status" => 400,
"message" => "Bad Request",
"errors" => ["page must be an integer"],
)
end
end
end
問題点
- 簡潔じゃない
Shared examplesを使って書くと
shared_examples "respond with an API response" do |status, message, data: nil, errors: nil|
it "respond with an API response" do
expect(response).to have_http_status 200
expect(response.content_type).to eq "application/json"
expect(response.body).to be_json include(
"status" => status,
"message" => message,
"data" => data,
"errors" => errors,
)
end
end
# テストケースはここから下
it_behaves_like "respond with an API response", 401, "Unauthorized"
context "when logged in" do
include_context "logged in"
context "when page is 1" do
let(:page) { 1 }
it_behaves_like "respond with an API response", 200, "Succeed", data: ...
end
context "when page is 'a'" do
let(:page) { "a" }
it_behaves_like "respond with an API response", 400, "Bad Request", errors: ["page must be an integer"]
end
end
問題点
-
簡潔じゃない=> 解決 - コンテキストに依存している(it_behaves_likeのコンテキストで動くため)
- テスト対象が何か分からない(responseを暗黙的に使っているため)
カスタムマッチャーを使って書くと
# be_api_response_ofカスタムマッチャーの定義は長くなるので省略
it { expect(response).to be_api_response_of(401, "Unauthorized") }
context "when logged in" do
include_context "logged in"
context "when page is 1" do
let(:page) { 1 }
it { expect(response).to be_api_response_of(200, "Succeed").with_data(...) }
end
context "when page is 'a'" do
let(:page) { "a" }
it { expect(response).to be_api_response_of(400, "Bad Request").with_error("page must be an integer") }
end
end
問題点
-
簡潔じゃない=> 解決 -
コンテキストに依存している(it_behaves_likeのコンテキストで動くため)=> 解決 -
テスト対象が何か分からない(responseを暗黙的に使っているため)=> 解決 - テスト条件がDRY?じゃない
カスタムマッチャーとRSpec::ContextHelperを使って書くと
# be_api_response_ofカスタムマッチャーの定義は長くなるので省略
example { expect(response).to be_api_response_of(401, "Unauthorized") }
context_with _shared: "logged in" do
example_with(page: 1) { expect(response).to be_api_response_of(200, "Succeed").with_data(...) }
example_with(page: "a") { expect(response).to be_api_response_of(400, "Bad Request").with_error("page must be an integer") }
end
問題点
-
簡潔じゃない=> 解決 -
コンテキストに依存している(it_behaves_likeのコンテキストで動くため)=> 解決 -
テスト対象が何か分からない(responseを暗黙的に使っているため)=> 解決 -
テスト条件がDRY?じゃない=> 解決 - 1行で書くと横に長くなる => いまどき少しは横に長くなってもいいよね?
- 英文のように読めない => 何かいいアイディアが思いついたら教えて欲しいです🙇
まとめ
-
shared_examples
は読みにくくなるので、あまり使わない方がいい - テストケースを見てテスト内容を理解できるカスタムマッチャーが作れる場合は、使った方が読みやすい
ただし、カスタムマッチャーに対してもテストを書く必要があると思います
ちなみに、コード例で使っているbe_json
マッチャーはSaharspecにあります - 条件を切り替えつつ行うテストを簡潔に書けるようにRSpec::ContextHelperを作ったのですが、どうでしょうか?
100行ほどのコードなので自分で好きなように改変するのも簡単だと思います
他に思うこと
-
subject/is_expected
はテスト対象が何か分からないので、読みにくいと思います - RSpec::Parameterizedも使われていますが、たいていのケースではRSpec::ContextHelperの方が読みやすくなるのではないかと思っています