RSpecでAPIのレスポンスボディのプロパティを検証する方法

Ruby

REST APIのテストコードにレスポンスボディの検証を追加することで、レスポンスボディに修正が加わったことを機械的にチェックできます。
今回はRSpecでREST APIのレスポンスボディを検証する方法について紹介します。

検証で利用するサンプルコードについて

検証用のAPIは、APIモードで作成したRailsアプリケーション上でscaffoldを実行することで作成しました。
今回はEventというモデルのAPIを作成しました。

$ rails g scaffold event title:string

RSpecは以下のようになります。

spec/requests/events_spec.rb

require 'rails_helper'

describe 'Events', type: :request do

  describe 'GET /events' do
    let!(:event) { create :event }
    before do
      get '/events'
    end
    it '取得できること' do
      expect(response).to have_http_status 200
      expect(JSON.parse(response.body).length).to eq(1)
    end
  end

  describe 'GET /events/:id' do
    let!(:event) { create :event }
    before do
      get "/events/#{event.id}"
    end
    it '取得できること' do
      expect(response).to have_http_status 200
    end
  end

  describe 'POST /events' do
    before do
      post "/events", params: { event: { title: 'サンプルイベント' } }
    end
    it '登録できること' do
      expect(response).to have_http_status 201
      expect(Event.all.count).to eq 1
    end
  end

  describe 'PATCH/PUT /events/:id' do
    let!(:event) { create :event }
    before do
      patch "/events/#{event.id}", params: { event: { title: 'サンプルイベント更新' } }
    end
    it '更新できること' do
      expect(response).to have_http_status 200
    end
  end

  describe 'DELEET /events/:id' do
    let!(:event) { create :event }
    before do
      delete "/events/#{event.id}"
    end
    it '削除できること' do
      expect(response).to have_http_status 204
      expect(Event.all.count).to eq 0
    end
  end
end

下準備: RSpec実行環境の構築

今回はテスティングフレームワークにRSpecを利用します。
また、テストデータを作成するためにFactoryBotを導入します。

実際に手元でRSpecの挙動を確認したい場合は以下のセットアップを行ってください。

Gemfile

group :development, :test do
  gem 'rspec-rails'
  gem 'factory_bot_rails'
end
$ bundle install
$ rails generate rspec:install

spec/rails_helper.rb

RSpec.configure do |config|
  # 以下を追加
  #  - FactoryBotのシンタックスを省略できるようにする
  #  - 例. FactoryBot.build(:user) → build(:user)
  config.include FactoryBot::Syntax::Methods
end

spec/factories/events.rb

FactoryBot.define do
  factory :event do
    title { "サンプルイベント" }
  end
end

レスポンスボディの検証方法

FactoryBotで作成したテストデータの値をexpected、APIを経由して取得したデータの値をactualとし、それぞれをmatchで検証します。

イベント詳細API(GET /events/:id)のRSpecは以下のようになります。

spec/requests/events_spec.rb

describe 'GET /events/:id' do
  let!(:event) { create :event }
  let(:expected_response_object) do
    {
      'id' => "#{event.id}".to_i,
      'title' => "#{event.title}",
      'created_at' => "#{event.created_at.strftime('%Y-%m-%dT%H:%M:%S.%3NZ')}",
      'updated_at' => "#{event.updated_at.strftime('%Y-%m-%dT%H:%M:%S.%3NZ')}"
    }
  end
  before do
    get "/events/#{event.id}"
  end
  it '取得できること' do
    expect(response).to have_http_status 200
    expect(JSON.parse(response.body)).to match(expected_response_object)
  end
end

レスポンスボディの値は型だけチェックできれば問題ないという場合はa_kind_ofを利用します。
イベント一覧API(GET /events)のRSpecは以下のようになります。
なお、イベント一覧APIは配列でデータが返されるのでall(match(...))で各オブジェクトのプロパティをチェックしています。

spec/requests/events_spec.rb

describe 'GET /events' do
  let!(:event) { create :event }
  let(:json) { JSON.parse(response.body) }
  let(:expected_response_properties) do
    {
      'id' => a_kind_of(Integer),
      'title' => a_kind_of(String),
      'created_at' => a_kind_of(String),
      'updated_at' => a_kind_of(String),
    }
  end
  before do
    get '/events'
  end
  it '取得できること' do
    expect(response).to have_http_status 200
    expect(json.length).to eq(1)
    expect(json).to all(match(expected_response_properties))
  end
end

プロパティが多すぎて全て記載するのが大変な場合や、プロパティの一部だけ確認できれば問題ないという場合はmatchの代わりにincludeを利用します。

includeを利用したRSpecは以下の通りです。

spec/requests/events_spec.rb

describe 'GET /events/:id' do
  let!(:event) { create :event }
  let(:expected_response_object) do
    {
      'title' => "#{event.title}"
    }
  end
  before do
    get "/events/#{event.id}"
  end
  it '取得できること' do
    expect(response).to have_http_status 200
    expect(JSON.parse(response.body)).to include(expected_response_object)
  end
end

参考: レスポンスボディの検証を追加したCRUD APIのRSpec全体像

冒頭で紹介したRSpecに、レスポンスボディの検証を追加したものが以下になります。

spec/requests/events_spec.rb

require 'rails_helper'

describe 'Events', type: :request do
  let(:json) { JSON.parse(response.body) }

  describe 'GET /events' do
    let!(:event) { create :event }
    let(:expected_response_object) do
      {
        'id' => "#{event.id}".to_i,
        'title' => "#{event.title}",
        'created_at' => "#{event.created_at.strftime('%Y-%m-%dT%H:%M:%S.%3NZ')}",
        'updated_at' => "#{event.updated_at.strftime('%Y-%m-%dT%H:%M:%S.%3NZ')}"
      }
    end
    let(:expected_response_properties) do
      {
        'id' => a_kind_of(Integer),
        'title' => a_kind_of(String),
        'created_at' => a_kind_of(String),
        'updated_at' => a_kind_of(String),
      }
    end
    before do
      get '/events'
    end
    it '取得できること' do
      expect(response).to have_http_status 200 # ステータスコードの検証
      expect(json[0]).to match(expected_response_object) # レスポンスデータの検証
      expect(json).to all(match(expected_response_properties)) # レスポンスのプロパティの検証
      expect(json.length).to eq(1) # オブジェクト数の検証
    end
  end

  describe 'GET /events/:id' do
    let!(:event) { create :event }
    let(:expected_response_object) do
      {
        'id' => "#{event.id}".to_i,
        'title' => "#{event.title}",
        'created_at' => "#{event.created_at.strftime('%Y-%m-%dT%H:%M:%S.%3NZ')}",
        'updated_at' => "#{event.updated_at.strftime('%Y-%m-%dT%H:%M:%S.%3NZ')}"
      }
    end
    before do
      get "/events/#{event.id}"
    end
    it '取得できること' do
      expect(response).to have_http_status 200
      expect(json).to match(expected_response_object)
    end
  end

  describe 'POST /events' do
    before do
      post "/events", params: { event: { title: 'サンプルイベント' } }
    end
    let(:expected_response_object) do
      event = Event.last
      {
        'id' => "#{event.id}".to_i,
        'title' => "#{event.title}",
        'created_at' => "#{event.created_at.strftime('%Y-%m-%dT%H:%M:%S.%3NZ')}",
        'updated_at' => "#{event.updated_at.strftime('%Y-%m-%dT%H:%M:%S.%3NZ')}"
      }
    end
    it '登録できること' do
      expect(response).to have_http_status 201
      expect(json).to match(expected_response_object)
      expect(Event.all.count).to eq 1 # DBへの反映確認
    end
  end

  describe 'PATCH/PUT /events/:id' do
    let!(:event) { create :event }
    before do
      patch "/events/#{event.id}", params: { event: { title: 'サンプルイベント更新' } }
    end
    let(:expected_response_object) do
      event = Event.last
      {
        'id' => "#{event.id}".to_i,
        'title' => "サンプルイベント更新",
        'created_at' => "#{event.created_at.strftime('%Y-%m-%dT%H:%M:%S.%3NZ')}",
        'updated_at' => "#{event.updated_at.strftime('%Y-%m-%dT%H:%M:%S.%3NZ')}"
      }
    end
    it '更新できること' do
      expect(response).to have_http_status 200
      expect(json).to match(expected_response_object)
    end
  end

  describe 'DELEET /events/:id' do
    let!(:event) { create :event }
    before do
      delete "/events/#{event.id}"
    end
    it '削除できること' do
      expect(response).to have_http_status 204
      expect(response.body).to be_empty
      expect(Event.all.count).to eq 0
    end
  end
end

さいごに

Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!