APIテストの書き方と共通テストの書き方

## APIのテスト
APIのテストに置いて、何をテストするのかというと、
レスポンスの``ステータスコード``と実際のデータを含んだ``レスポンスボディ``をテストする。

**◆ステータスコード**
APIによって返されるステータスコードは通常以下の４つに分類される。

``200:OK``
リクエストは成功し、レスポンスとともに要求に応じた情報が返される。

``401:Unauthorized``
認証失敗。認証が必要。

``403:Forbidden``
禁止されている。リソースにアクセスすることを拒否されている。

``404:Not Found ``
未検出。リソースが見つからなかった。

**◆レスポンスボディ**
``GETリクエスト``の場合は単にリクエストボディに要求したデータが入っているかをテストする。

``POST``、``PUT``、``DELETE``　リクエストの際にはそれぞれ要求したデータ通りの動きをするかテストする。

## テストツール
APIだからといって特別なテストツール等は使用する必要はなく通常のrailsアプリで頻繁に使用される``rspec``を使用してテストする。

※通常Railsで結合テストを行う際には``capybara``というgemがよく使われるが、こちらはAPIのテストをする際には適していないので注意。（capybara = UIテストのためのrubyフレームワークのため）

APIに対するリクエストに関してテストなので``rspec/requests/``ディレクトリ下にテストを作成していく。

## GETリクエスト
GETリクエストでは正しくデータを取得した際に、``ステータスコード200``が返るか、正しく要求したデータが取得できたかをテストする。

#### indexなどで一覧を取得する際の挙動
```spec/requests/api/v1/posts_spec.rb
require 'rails_helper'

describe 'PostAPI' do
  it '全てのポストを取得する' do
    FactoryBot.create_list(:post, 10)

    get '/api/v1/posts'
    json = JSON.parse(response.body)

    # リクエスト成功を表す200が返ってきたか確認する。
    expect(response.status).to eq(200)

    # 正しい数のデータが返されたか確認する。
    expect(json['data'].length).to eq(10)
  end
end
```

#### showなどで``/api/v1/posts/:id``に対するテスト
```spec/requests/api/v1/posts_spec.rb
require 'rails_helper'

describe 'PostAPI' do
  it '特定のpostを取得する' do
    post = create(:post, title: 'test-title')

    get "/api/v1/posts/#{post.id}"
    json = JSON.parse(response.body)

    # リクエスト成功を表す200が返ってきたか確認する。
    expect(response.status).to eq(200)

    # 要求した特定のポストのみ取得した事を確認する
    expect(json['data']['title']).to eq(post.title)
  end
end
```

## POSTリクエスト
POSTリクエストでは正しくデータが作成できたか、正しくデータを作成した際に``ステータスコード200``が返るかをテストする。
```spec/requests/api/v1/posts_spec.rb
require 'rails_helper'

describe 'PostAPI' do
 it '新しいpostを作成する' do
    valid_params = { title: 'title' }

    #データが作成されている事を確認
    expect { post '/api/v1/posts', 
             params: { post: valid_params } 
           }.to change(Post, :count).by(+1)

    # リクエスト成功を表す200が返ってきたか確認する。
    expect(response.status).to eq(200)
  end
end
```

## PUTリクエスト
PUTリクエストでは正しくデータを編集した際に``ステータスコード200``が返るか、正しくしたデータが編集できたかをテストする。

```spec/requests/api/v1/posts_spec.rb
require 'rails_helper'

describe 'PostAPI' do
  it 'postの編集を行う' do
    post = create(:post, title: 'old-title')

    put "/api/v1/posts/#{post.id}", params: { post: {title: 'new-title'}  }
    json = JSON.parse(response.body)

    # リクエスト成功を表す200が返ってきたか確認する。
    expect(response.status).to eq(200)

    #データが更新されている事を確認
    expect(json['data']['title']).to eq('new-title')
 end
end
```

## DELETEリクエスト 
DELETEリクエストでは正しくしたデータが削除できたか、正しくデータを削除した際に``ステータスコード200``が返るかをテストする。
```spec/requests/api/v1/posts_spec.rb
require 'rails_helper'

describe 'PostAPI' do
  it 'postを削除する' do
    post = create(:post)

    #データが削除されている事を確認
    expect { delete "/api/v1/posts/#{post.id}" }.to change(Post, :count).by(-1)

    # リクエスト成功を表す200が返ってきたか確認する。
    expect(response.status).to eq(200)
  end
end
```

## APIテストに置いて便利そうなのを紹介
#### JSON.parse(response.body)
railsアプリをAPIで実装する場合、ほとんどがJSON形式で返される。その際テストで毎回``JSON.parse(response.body)``と記入するのが面倒なので、その共通化を行う。

```spec/support/response_helper.rb
module ResponseHelper
  def body
    JSON.parse(response.body)
  end
end
```
上記ファイルが記入できたら、このままでは使えないので、``rails_helper.rb``に使えるように記述を加える。

```spec/rails_helper.rb
# This file is copied to spec/ when you run 'rails generate rspec:install'
require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'

require File.expand_path('../config/environment', __dir__)

# Prevent database truncation if the environment is production
abort("The Rails environment is running in production mode!") if Rails.env.production?
require 'rspec/rails'
Dir[Rails.root.join('spec/support/*.rb')].sort.each { |f| require f }
#この１行と↑
.
.
RSpec.configure do |config|
  config.include ResponseHelper
   #↑この１行を追記
.
.
end
```
``Dir[Rails.root.join~``と記載することで、テストの冒頭に``require 〇〇``と記載せずに済む。

``RSpec.configure do |config|``の配下に上記を追記することで、``spec/support/``配下のものが呼び出せるようになり、

``JSON.parse(response.body)``を、``body``の一言で呼び出せて便利になる。

#### 共通テスト
テストのよくある内容として、ログイン・未ログインの分岐等の、どのアクションにもログイン済みか？というテストがあるが、

上記のような未ログインについてのテストは他のspecでも使うことが予想できるので``shared_example``を使ってテストを共通化させる。

使い方は、``shared_examples_for 'テスト命名' do ~ end``内に共通させたいテストを記載する。

```spec/support/example/unauthorized_user.rb
# let(:http_request) { get api_path }

shared_examples_for 'unauthorized user' do
  context 'when user has not signed in' do
    it 'returns 401 errors' do
      http_request
      expect(response.body).to eq("HTTP Token: Access denied.\n")
      expect(response).to have_http_status(401)
    end
  end
end
```
共通テストが出来たら、実際にそのテストを呼び指す際には``it_should_behave_like 'テスト名'``と記載すると呼び出せる。

なお``it_should_behave_likeメソッド``は新しい``context``を生成し、そこにテストコードを埋め込むような形になっているので、``it``内に間違って記載しないよう注意。

```spec/requests/api/v1/article_spec.rb
RSpec.describe 'Api::V1::Articles', type: :request do
  let!(:user) { create(:user) }
  let(:headers) { { CONTENT_TYPE: 'application/json',
                    ACCEPT: 'application/json' } }

  describe 'index: GET /articles' do
    let(:article_num) { 10 }
    let(:http_request) { get api_v1_articles_path, headers: headers }

    before do
      create_list(:article, article_num, user: user)
    end

    context 'アクセストークン発行' do
      let!(:api_key) { create(:api_key, user: user) }
      let!(:headers) { { CONTENT_TYPE: 'application/json',  ACCEPT: 'application/json', Authorization: "Bearer #{api_key.access_token}" } }
      
      it '正常に作動' do
        http_request
        expect(body['data'].count).to eq(article_num)
        expect(response).to be_successful
        expect(response).to have_http_status(:ok)
      end
    end
    # ここ↓
    it_should_behave_like 'unauthorized user'

  end
```

また今回はファイルを分けて``shared_example``を記入したので、先ほどの``JSON.parse``と同じように``rails_helper.rb``ファイルに記述がいる。

```spec/rails_helper.rb
# This file is copied to spec/ when you run 'rails generate rspec:install'
require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'

require File.expand_path('../config/environment', __dir__)

# Prevent database truncation if the environment is production
abort("The Rails environment is running in production mode!") if Rails.env.production?
require 'rspec/rails'
Dir[Rails.root.join('spec/support/*.rb')].sort.each { |f| require f }
#↑さっき書いたJSONの設定
Dir[Rails.root.join('spec/support/example/*.rb')].sort.each { |f| require f }
#↑この１行を追加
.
.
end
```

## まとめ
・APIテストは、``ステータスコード``と``レスポンスボディ``をテストする
・APIテストは、``rspec``を使用し``rspec/requests/``ディレクトリ下にテストを作成する

実際に現場でテストを書くようになる場合、下記記事が役に立ちそう。
https://mogulla3.tech/articles/2019-08-29-01/#request-spec

またAPIテストであれば、認証系は必ず絡んでくると思うので、こちらの記事も見ておいた方が良さそう。

https://qiita.com/h_tyokinuhata/items/ab8e0337085997be04b1
## 参考記事
[【Rails】APIテストの書き方](https://qiita.com/k-penguin-sato/items/defdb828bd54729272ad#%E6%A6%82%E8%A6%81)
[【RSpec】モジュールの使用方法について](https://boku-boc.hatenablog.com/entry/2021/02/06/122348)
[【RSpec】コードの重複を回避することができるit_behaves_likeメソッドの使い方について解説](https://techtechmedia.com/it_behaves_like-rspec/)