RSpec の便利な機能を使って、わかりやすく簡潔な API のテストを書こう！


# はじめに

自分がこれまで API 向けの RSpec を書いてきて、これは良いな！と思った記述の仕方をご紹介します。
うまく活用することで、わかりやすく簡潔な記述ができるかと思います。


## RSpec と Ruby のバージョン

- RSpec 3.8.0
- Ruby 2.4.0


# `subject` を活用する

条件が異なるテストがあり、同じ API 呼び出し (`before { get '/users' }`) が
複数箇所に現れてしまったそんなとき！

```ruby:beforeをDRYにしたくなる...
  describe '#index' do
    context 'with 1 user' do
      let!(:user) { create(:user) }
      before { get '/users' }
      it { expect(response.status).to eq 200 }
    end

    context 'with 2 users' do
      let!(:users) { create_list(:user, 2) }
      before { get '/users' }
      it { expect(response.status).to eq 200 }
    end
  end
```

下記のようにした場合、API 呼び出しが条件設定前(ユーザー作成前)に実行されるので、
意図したテストになりません :disappointed: 

```ruby:beforeを括りだしたが意図とは異なるテストになってしまった…
  describe '#index' do
    before { get '/users' } # ユーザー作成前に実行されてしまう

    context 'with 1 user' do
      let!(:user) { create(:user) }
      it { expect(response.status).to eq 200 }
    end

    context 'with 2 users' do
      let!(:users) { create_list(:user, 2) }
      it { expect(response.status).to eq 200 }
    end
  end
```


`subject` & `is_expected` を活用すると、DRY にできます！
また、どの API 呼び出しに対するテストなのかも分かりやすくなると個人的に感じています :thumbsup: 

```ruby:subjectを活用した書き方
  describe '#index' do
    subject { get '/users' }

    context 'with 1 user' do
      let!(:user) { create(:user) } 
      it { is_expected.to eq 200 } # ここで subject が評価される
    end

    context 'with 2 users' do
      let!(:users) { create_list(:user, 2) }
      it { is_expected.to eq 200 } # ここで subject が評価される
    end
  end
```

## 注意点

`subject` が評価されるのは `is_expected` が呼ばれたときなので、
`response` を確認する場合には順序にご注意を。

```ruby
    context 'with 2 users' do
      let!(:users) { create_list(:user, 2) }
      it do
        is_expected.to eq 200
        response_json = JSON.parse(response.body) # subject が評価されている必要あり
        expect(response_json.length).to eq 2
      end
    end
```

# `change` マッチャの活用

「API 呼び出しによって、何かの変化を確認したい！」場合には、`change` マッチャ！
例として、ユーザー数の変化を確認するテストを作成してみます。

愚直に書くと、下記のように、API 呼び出し前後での `User.count` の値を確認することに… :sweat: 

```ruby:愚直にUserの増減を確認するテスト
  describe 'POST /users' do
    context 'with correct parameters' do
      let(:params) { { name: 'name', email: 'hoge@hoge' } }

      it 'should create 1 user' do
        expect(User.count).to eq 0
        post '/users', params: params
        expect(User.count).to eq 1
      end
    end

    context 'with empty parameters' do
      let(:params) { {} }

      it 'should not create any user' do
        expect(User.count).to eq 0
        post '/users', params: params
        expect(User.count).to eq 0
      end
    end
  end
```

`change` マッチャを活用すると下記のように記述できます。
何によって、何の値が、どのように変化することを期待しているのか、が分かりやすいのではないでしょうか :smiley: 
(ただし、`change` マッチャは `expect` が長くなりやすいので、諸刃の剣感があります)

```ruby:changeマッチャを活用した場合
  describe 'POST /users' do
    context 'with correct parameters' do
      let(:params) { { name: 'name', email: 'hoge@hoge' } }

      it 'should create 1 user' do
        expect { post '/users', params: params }.to change(User, :count).from(0).to(1)
      end
    end

    context 'with empty parameters' do
      let(:params) { {} }

      it 'should not create any user' do
        expect { post '/users', params: params }.not_to change(User, :count).from(0)
      end
    end
  end
```

`subject` と組み合わせる場合には、`subject` をブロックにする必要があります。
スッキリしますね :smiley: 

```ruby:changeマッチャ+subjectを使う場合
  describe 'POST /users' do
    subject { -> { post '/users', params: params } }

    context 'with correct parameters' do
      let(:params) { { name: 'name', email: 'hoge@hoge' } }
      it { is_expected.to change(User, :count).from(0).to(1) }
    end

    context 'with empty parameters' do
      let(:params) { {} }
      it { is_expected.not_to change(User, :count).from(0) }
    end
  end
```


# `shared_context` + `include_context`

複雑な前提条件やよく使う前提条件を `shared_context` として定義しておくことで、
`include_context` で読み込むことができます。

例えば、以下のように複数のレコードが絡むような前提条件があると、見通しが少し悪くなってしまいます。

```ruby:同じ前提条件が出てくるテスト達
    context 'test case A' do
      let(:user) { create(:user) }
      let(:another_user) { create(:user) }
      let!(:another_user_follow) { create(:relationship, follower_id: another_user.id, followed_id: user.id) }

      it { is_expected.to eq 200 }
    end

    context 'test case B' do
      let(:user) { create(:user) }
      let(:another_user) { create(:user) }
      let!(:another_user_follow) { create(:relationship, follower_id: another_user.id, followed_id: user.id) }

      # (他の条件)

      it { is_expected.to eq 200 }
    end
```

前提条件を `share_context` で定義しておき、必要なテストで `include_context` を使い読み込むことで、
実際のテストの見通しが良くなります :thumbsup: 

```ruby:share_context+include_contextを使用した例
    shared_context 'followed_user' do
      let(:user) { create(:user) }
      let(:another_user) { create(:user) }
      let!(:user_follow) { create(:relationship, follower_id: another_user.id, followed_id: user.id) }
    end

    context 'test case A' do
      include_context 'followed_user'
      it { is_expected.to eq 200 }
    end

    context 'test case B' do
      include_context 'followed_user'

      # (他の条件)

      it { is_expected.to eq 200 }
    end
```

よく使う前提条件は、`spec/support/shared_contexts/` 以下に定義しておくと、
他のテストで利用できるので便利です。

`spec/support` 以下に置いて読み込む場合には、`rails_helper.rb` に下記のような記述が必要です。

```ruby
Dir[Rails.root.join("spec", "support", "**", "*.rb")].each { |f| require f }
```


# include を活用する

request spec でレスポンス中のパラメーターの値まで確認することは、場合によってはやりすぎです :sweat: 
ただし、API として最低限、レスポンスのキーが存在するかは確認しておきましょう。

```ruby:includeを活用した書き方
  describe '#index' do
    subject { get "/users/#{user.id}" }

    context 'with 1 user' do
      let(:user) { create(:user) }

      it do
        is_expected.to eq 200
        response_json = JSON.parse(response.body)
        expect(response_json).to include('id', 'name')
      end
    end
  end
```

レスポンスが配列の場合には、`all` を使うことで、`include` を各要素に適用できます :thumbsup: 

```ruby:配列の要素にincludeを適用する書き方
  describe '#index' do
    subject { get '/users' }

    context 'with 2 users' do
      let!(:users) { create_list(:user, 2) }

      it do
        is_expected.to eq 200
        response_json = JSON.parse(response.body)
        expect(response_json).to all(include('id', 'name'))
      end
    end
  end
```


# shared_examples/shared_examples_for

条件は異なるけれども、同じ結果を期待する場合には、`shared_examples` を使うことで DRY にできます :smiley: 
(`shared_examples_for` とも書くことができます)

```ruby:同じ期待値のテスト達
  describe '#index' do
    subject { get '/users' }

    context 'with 2 users' do
      let!(:users) { create_list(:user, 2) }

      it do
        is_expected.to eq 200
        response_json = JSON.parse(response.body)
        expect(response_json).to all(include('id', 'name'))
      end
    end

    context 'with 3 users' do
      let!(:users) { create_list(:user, 3) }

      it do
        is_expected.to eq 200
        response_json = JSON.parse(response.body)
        expect(response_json).to all(include('id', 'name'))
      end
    end
  end
```

`shared_examples` の内容を `it_behaves_like` で使用することができます。

```ruby:shared_examplesを活用した例
  describe '#index' do
    subject { get '/users' }

    shared_examples 'return proper response keys' do
      it do
        is_expected.to eq 200
        response_json = JSON.parse(response.body)
        expect(response_json).to all(include('id', 'name'))
      end
    end

    context 'with 2 users' do
      let!(:users) { create_list(:user, 2) }
      it_behaves_like 'return proper response keys'
    end

    context 'with 3 users' do
      let!(:users) { create_list(:user, 3) }
      it_behaves_like 'return proper response keys'
    end
  end
```

引数を使うこともできるので、下記のように似ているけれども微妙に異なるものも…

```ruby:似ているけれども微妙に異なる期待値のテスト達
  describe '#index' do
    subject { get '/users' }

    context 'with 2 users' do
      let!(:users) { create_list(:user, 2) }

      it do
        is_expected.to eq 200
        response_json = JSON.parse(response.body)
        expect(response_json).to all(include('id', 'name'))
        expect(response_json.length).to eq 2
      end
    end

    context 'with 3 users' do
      let!(:users) { create_list(:user, 3) }

      it do
        is_expected.to eq 200
        response_json = JSON.parse(response.body)
        expect(response_json).to all(include('id', 'name'))
        expect(response_json.length).to eq 3
      end
    end
  end
```

下記のように `shared_examples` で書くことができます :smile: 

```ruby:shared_examplesと引数を活用した例
  describe '#index' do
    subject { get '/users' }

    shared_examples 'return proper response keys' do |user_count|
      it do
        is_expected.to eq 200
        response_json = JSON.parse(response.body)
        expect(response_json).to all(include('id', 'name'))
        expect(response_json.length).to eq user_count
      end
    end

    context 'with 2 users' do
      let!(:users) { create_list(:user, 2) }
      it_behaves_like 'return proper response keys', 2
    end

    context 'with 3 users' do
      let!(:users) { create_list(:user, 3) }
      it_behaves_like 'return proper response keys', 3
    end
  end
```

## shared_examples の注意点

`shared_examples` は便利なのですが、使いすぎると見通しが悪くなってしまうこともあると感じています :disappointed: 
うまく活用して、全体として理解しやすい記述を目指したいですね :smile: 


# カスタムマッチャを活用する

カスタムマッチャを活用することで、期待値判定の記述を簡潔にすることができます。
特に、独自のエラーレスポンスを定義している場合には、カスタムマッチャ導入で捗りそうです :muscle: 

例えば、下記のような、エラーレスポンスを確認するテストがあるとします。

```ruby:エラー時のレスポンスの確認
  describe '#index' do
    subject { get '/users/0' }

    context 'with no user' do
      it do
        is_expected.to eq 404
        response_json = JSON.parse(response.body)
        expect(response_json['error']['code']).to eq 111
        expect(response_json['error']['message']).to eq 'not found'
      end
    end
  end
```

エラーレスポンスを確認するカスタムマッチャを定義しておくと、下記のように書くことができます :smile: 

```ruby:カスタムマッチャ活用の例
  describe '#index' do
    subject { get '/users/0' }

    context 'with no user' do
      it do
        is_expected.to eq 404
        expect(response).to be_error_json(code: 111, message: 'not found')
      end
    end
  end
```

カスタムマッチャの定義は下記のようになっています。

```ruby:カスタムマッチャの例
RSpec::Matchers.define :be_error_json do |code:, message:|
  match do |actual|
    json_response = JSON.parse(actual.body)
    expect(json_response['error']['code']).to eq code
    expect(json_response['error']['message']).to eq message
  end
end
```

カスタムマッチャは、`spec/support/matchers.rb` などに定義すると良いでしょう :thumbsup: 
`spec/support` 以下に置いて読み込む場合には、`rails_helper.rb` に下記のような記述が必要です。

```ruby
Dir[Rails.root.join("spec", "support", "**", "*.rb")].each { |f| require f }
```

# おわりに

以上、APIのテストで活用したい記述方式でした！

RSpec の記述方式は、チームによって方針があったり、
良い感じに記述できた！と思ったら、後から見たら分かりづらくなっていたり…ということがあるので、
過度に DRY にせず、便利な機能も用法用量を守って適切に使うのが良いかと思います :smile: 

今回ご紹介した以外にも便利な機能はたくさんあるので、ぜひ活用して、楽しいテストライフをお過ごしください〜。