JSON:APIのRequestSpecに、jsonapi-rspecを導入する

f:id:takuyawww1101:20201221141505p:plain

目次

  • はじめに
  • jsonapi-rspecのinstall
  • 既存のRequestSpecによく見られるテストケースの例
  • jsonapi-rspecで置き換えてみる
  • さいごに

はじめに

こんにちは、株式会社スタメンでエンジニアをしているワカゾノです。 4月からサーバーサイドエンジニアとして、弊社プロダクトTUNAGの開発を行っております。

TUNAGでは、ユーザビリティの向上を目的に、既存機能のReact化、Native化が進められています。 その際に、jsonapi_serializerというGemを使用してAPI実装を行っています。responseはJSON:API形式で取得することが出来ます。

また、弊社ではAPIドキュメント化を進めており、RequestSpecによる結合テストを必ず追加し、APIインターフェースに対してドキュメントを作成、追加するようにしています。

TUNAGの既存のRequestSpecには下記のような問題点があります。

  • responseのstatusやID値のみのテストケースが多く、APIドキュメントに記載されている各属性値に関するテストケースが少ない
  • responseのネストが深くなる場合に、response['data'][0]['a']['b']のように対象のテストデータを取得する必要があるため、テストを作成した人以外が見た場合に、直感的に分かりにくいテストケースになってしまう

今後、APIを実装する機会が増加する、APIドキュメントの各属性値に関して、正しさを担保する為に、より細かく、そして直感的にRequestSpecを書きたいというニーズがありました。

そのため、これらの問題点を解消するために、今回はjsonapi-rspecというGemを試してみました。

その使用感や感想についてまとめてみようと思います。

jsonapi-rspecのinstall

公式のREADME通りですが、下記の手順でinstall、設定をします。

  • Gemfileに追記します
gem 'jsonapi-rspec'
  • プロジェクトにinstallします
bundle install
  • spec/spec_helpers.rbに設定を追記します
  • テストケースの中でkeyをstring型、symbol型のどちらも使用したい場合はconfig.jsonapi_indifferent_hash = trueと設定します。
# spec/spec_helpers.rb
require 'jsonapi/rspec'

RSpec.configure do |config|
  config.include JSONAPI::RSpec

  # Support for documents with mixed string/symbol keys. Disabled by default.
  config.jsonapi_indifferent_hash = true
end

既存のRequestSpecによく見られるテストケースの例

下記のようなTODOアプリケーションを例として、テストケースを作成しました

  • /api/v1/tasksにアクセスした際に、TODOリスト一覧を取得することが出来る
  • TODOリストの中には、「完了」と「未完了」のタスクがあり、/api/v1/tasksのエンドポイントにパラメータを付与することで、絞り込みを行うことが可能
require 'rails_helper'

RSpec.describe 'Api::V1::Tasks', type: :request do
    describe 'GET /api/v1/tasks' do
        # 完了しているTODOタスクデータを作成
        let!(:complete_todo_task) # 詳細は割愛
        
        # 未完了のTODOタスクデータを作成
        let!(:incomplete_todo_task) # 詳細は割愛
        
        # responseから、取得したデータIDを配列へ格納
        let(:json) { JSON.parse(response.body, symbolize_names: true)
        let(:response_todo_tasks) { json[:data].map { |task| task[:id].to_i } }
        
        context '正常系' do
            context 'パラメーターが存在しない場合' do                
                it 'TODOリストを取得する' do
                    get 'api/v1/tasks'
                    expect(response_todo_tasks).to eq [complete_todo_task.id, incomplete_todo_task.id]
                end
            end
            context 'パラメーターが存在する場合' do
                context '完了のパラメーターを付与した場合' do
                    it '完了しているTODOデータのみ取得する' do
                        get 'api/v1/tasks', params: { status: 'complete' }
                        expect(response_todo_tasks).to eq [complete_todo_task.id]
                    end
                end
                context '未完了のパラメーターを付与した場合' do
                    it '未完了のTODOデータのみ取得する' do
                        get 'api/v1/○○○', params: { status: 'incomplete' }
                        expect(response_todo_tasks).to eq [incomplete_todo_task.id]
                    end
                end
            end
        end
    end
end

TUNAGの既存のRequestSpecではこのように、レスポンスから取得したデータのIDによりIN、OUT値をテストするテストケースが多く存在します。

今回のAPIのresponse例は下記のような構造となりますが、ID値以外の各属性値をテストしたい場合に、対象のデータを取得するまでが大変であり、またAPIのインターフェースが変更された際の、テストの修正点が多くなってしまいます。

{
    :data => [{
                    :id =>"○○○",
                    :type =>"tasks",
                    :attributes => {
                        :title=>"宿題を終わらせる!"
                        :status=>"完了",
                    }
               },
               {
                    :id =>"○○○",
                    :type =>"tasks",
                    :attributes => {
                        :title=>"買い物に行く!"
                        :status=>"未完了",
                    }
               }
              ],
    :meta => {
                    :total_todo_num => 2
              }
}

jsonapi-rspecで置き換えてみる

先程のテストケースをjsonapi-rspecを使用して置き換えたものが下記になります。

require 'rails_helper'

RSpec.describe 'Api::V1::Tasks', type: :request do
    describe 'GET /api/v1/tasks' do
        # データを作成する箇所は割愛
        
        # 属性毎のテストをするため変更
        let(:json) { JSON.parse(response.body, symbolize_names: true)
        let(:response_todo_tasks) { json[:data] }
        
        context '正常系' do
            context 'パラメーターが存在しない場合' do
                it 'TODOリストを取得する' do
                    get 'api/v1/tasks'
                    expect(response_todo_tasks[0]).to have_id(complete_todo_task.id)
                    expect(response_todo_tasks[1]).to have_id(incomplete_todo_task.id)
                end
                # 新規にmetaのテストケースを追加
                it 'metaデータを取得することが出来る' do
                    get 'api/v1/tasks'
                    expect(json).to have_meta(total_todo_num: 2)
                end
            end
            context 'パラメーターが存在する場合' do
                context '完了のパラメーターを付与した場' do
                    it '完了しているTODOデータのみ取得する' do
                        get 'api/v1/tasks', params: { status: 'complete' }
                        expect(response_todo_tasks[0]).to have_type('tasks')
                        expect(response_todo_tasks[0]).to have_id(complete_todo_task.id)
                        expect(response_todo_tasks[0]).to have_attribute(:title).with_value('宿題を終わらせる!')
                        expect(response_data[0]).to have_attribute(:status).with_value('完了')
                    end
                end
                context '未完了のパラメーターを付与した場合' do
                    it '未完了のTODOデータのみ取得する' do
                        get 'api/v1/tasks', params: { status: 'incomplete' }
                        expect(response_todo_tasks[0]).to have_type('tasks')
                        expect(response_todo_tasks[0]).to have_id(incomplete_todo_task.id)
                        expect(response_todo_tasks[0]).to have_attribute(:title).with_value('買い物に行く!')
                        expect(response_todo_tasks[0]).to have_attribute(:status).with_value('未完了')
                    end
                end
            end
        end
    end
end

マッチャとして下記を使用しています。

  • have_type
  • have_id
    • JSON:APIでリソースの判別に使用されるtypeidをテストするmatcher
  • have_attribute(key).with_value(value)
    • attributes配下にkeyが含まれるかとその値をテストするmatcher
  • have_meta
    • metaデータに関してkeyと値をテストするmatcher

その他にもリソース間の関連をテストする必要がある場合にhave_relationship().with_data()などのmatcherも用意されています。

APIのインターフェースに沿った形でテストケースを書くことが出来るため、より直感的にテストを書くことが出来るようになりました。

また、テスト作成者以外が見てもテストの意図が明確であり、インターフェースの変更に伴うテストの修正が容易になると思います。

さいごに

APIドキュメントは既存機能をReact化、Native化するにあたり、多くのチームが参照するため、APIのインターフェースの正確性を担保することが重要であり、なるべく詳細にテストを書く必要があると考えています。

JSON:APIのRequestSpecをより書きやすく、分かりやすくするために有用なGemだと思いますので、是非試してみてください!

スタメンでは一緒にプロダクト開発を進めてくれる仲間を募集しています! 興味を持っていただいた方は、是非下記の募集ページを御覧ください。

Webアプリケーションエンジニア募集ページ