リーダブルRSpec（カスタムマッチャー+RSpec::ContextHelper編）

## 読みやすいRSpec書いていますか？

---
### 読みやすいRSpecって何？

「簡潔でテストケースを見てテスト内容が理解しやすいもの」と考えています
ここでのテストケースとは、it/exampleブロックのことを指しています

---
## 例題
例えば、Rails製REST APIのRequest Specを書くことを考えてみます
コードは正しいか確認していないので、多少間違っていてもお許しください🙇

### 前提
* APIはたくさんある
* 良くないけど、クエリパラメータのチェックをcontrollerでしているため、テストはRequest Specで行う

---
### APIの共通仕様
* Server Errorを除き、HTTPステータスコードは必ず200で返す
* HTTPレスポンスボディはJSON形式で返す
  * 成功時のレスポンス
    ```json
    { "status": 200, "message": "Succeed", "data": ... }
    ```
  * 未認証時のレスポンス
    ```json
    { "status": 401, "message": "Unauthorized" }
    ```
  * リクエストパラメータが不正な時のレスポンス
    ```json
    { "status": 400, "message": "Bad Request", "errors": ["error", ...] }
    ```

---
### ユーザー一覧APIのSpec
`/users?page=1`のようにアクセスすると、ユーザー一覧を返すAPIのテストを書いていきます

#### まず、APIへのリクエストを書いて
```ruby
before do
  get "/users", params: { page: page }
end
let(:page) { 1 }
```

---
#### 単純に書くと

```ruby
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](https://relishapp.com/rspec/rspec-core/docs/example-groups/shared-examples)を使って書くと

```ruby
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を暗黙的に使っているため）

---
#### [カスタムマッチャー](https://relishapp.com/rspec/rspec-expectations/docs/custom-matchers)を使って書くと

```ruby
# 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](https://github.com/masaakiaoyagi/rspec-context_helper.rb)を使って書くと

```ruby
# 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](https://github.com/zverok/saharspec#be_jsonvalue-and-be_json_symvalue-matchers)にあります
* 条件を切り替えつつ行うテストを簡潔に書けるように[RSpec::ContextHelper](https://github.com/masaakiaoyagi/rspec-context_helper.rb)を作ったのですが、どうでしょうか？
  100行ほどのコードなので自分で好きなように改変するのも簡単だと思います

---
### 他に思うこと
* `subject/is_expected`はテスト対象が何か分からないので、読みにくいと思います
* [RSpec::Parameterized](https://github.com/tomykaira/rspec-parameterized)も使われていますが、たいていのケースではRSpec::ContextHelperの方が読みやすくなるのではないかと思っています

---
## 読みやすいテストを書いていきたいですね