読者です 読者をやめる 読者になる 読者になる

一から勉強させてください( ̄ω ̄;)

最下級エンジニアが日々の学びをアウトプットしていくだけのブログです。

Grape + Rails4.2のAPI開発でエンドポイントとエンティティのテストについて考える

最近、Grape + Rails (v4.2.6)でRESTなAPIを開発する機会があったのですが、今回はその開発の中で悩んだ、「Grapeのエンドポイントとエンティティのテスト」について書きたいと思います。

前提としてGrapeののエンティティは grape-entity、テストフレームワークrspecを採用しているものとします。またAPIのformatはJSONとします。

APIの実装

例として、ユーザーがひたすら無意味に投稿しまくるだけのシンプルなアプリケーションを考えてみる。

モデル

# User
class User < ActiveRecord::Base
  has_many :posts, inverse_of: :user
  
  validates :name, presence: true
end

# Post
class Post < ActiveRecord::Base
  belongs_to :user, inverse_of: :posts
  
  validates :user, presence: true
  validates :text, presence: true
  
  scope :latest, -> { order created_at: :desc }
end

エンティティ

# User
module API
  module V1
    module Entities
      class User < Grape::Entity
        expose :id
        expose :name
      end
    end
  end
end

# Post
module API
  module V1
    module Entities
      class Post < Grape::Entity
        expose :id
        expose :text
        expose :user, using: Entities::User
        expose :created_at
        expose :updated_at
      end
    end
  end
end

エンドポイント

# /api/v1/postsでPostの一覧が返ることを想定,
module API
  module V1
    class Posts < Grape::API
      resources :posts do
        desc 'Return all posts' do
          success API::V1::Entities::Post
        end
        get do
          posts = Post.includes(:user).latest
          present(posts, with: API::V1::Entities::Post)
        end
      end
    end
  end
end
# /api/v1/postsのレスポンスのサンプル
[
  {
    "id": 2,
    "text": "ひゃっはーー",
    "user": {
      "id": 1,
      "name": "Tom"
    },
    "created_at": "2016-08-09T07:22:35.000Z",
    "updated_at": "2016-08-09T07:22:35.000Z"
  },
  {
    "id": 1,
    "text": "うぇーーい",
    "user": {
      "id": 2,
      "name": "Bob"
    },
    "created_at": "2016-08-08T07:22:35.000Z",
    "updated_at": "2016-08-08T07:22:35.000Z"
  }
]

APIのテスト

方針

  • Grapeではモデルとエンティティを指定してpresentメソッドを呼ぶといい感じにbody(今回はJSON)を返してくれる
  • このpresentメソッドをテスト環境で使えれば、各エンティティ、エンドポイントで返却されるであろうJSONを組み立ててテストできそう
  • JSONのテストはjson_spec使えばよさそう
  • Rspectype: :apiを定義して、API専用ヘルパーメソッドを読み込んでやればよさそう
  • このあたりを解読すればいけそう

API用ヘルパー

# rails_helperで読み込んでおく
module API
  module TestHelpers
    include Rack::Test::Methods
    include Grape::DSL::InsideRoute
    
    private

    def app
      Rails.application
    end

    def request
      last_request
    end

    def response
      last_response
    end

    def render_api_response *args
      # Grape::DSL::InsideRouteモジュールをincludeしているのでpresentメソッドをそのまま使える
      present(*args).to_json.tap do
        # presentメソッドを呼ぶとbodyがインスタンス変数に入るようになっているが、テストでは返却されるJSONがわかればよいだけなので@bodyは常にnilにしておく
        @body = nil
      end
    end

    # Override
    def entity_representation_for entity_class, object, options
      # presentメソッドの中で呼ばれるメソッド
      # 実際はここでenvをセットするような処理が入っているが、Rack::Testのenvメソッドと名前があたってArgumentErrorが発生するので無視する
      entity_class.represent object, options
    end
  end
end

RSpec.configure do |config|
  config.include API::TestHelpers, type: :api
end

エンティティのテスト

require 'rails_helper'

RSpec.describe API::V1::Entities::Post, type: :api do
  describe 'fields' do
    subject { render_api_response(post, with: described_class) }

    let(:user) { create(:user) } # FactoryGirlが入っている体でおねがいします
    let(:post) { create(:post, user: user) }
    let(:expected_user) { render_api_response(user, with: API::V1::Entities::User) }

    it do
      is_expected.to be_json_eql(post.id).at_path 'id'
      is_expected.to be_json_eql(post.text.to_json).at_path 'text'
      is_expected.to be_json_eql(post.created_at.to_json).at_path 'created_at'
      is_expected.to be_json_eql(post.updated_at.to_json).at_path 'updated_at'
      is_expected.to be_json_eql(expected_user).at_path 'user'
    end
  end
end

エンドポイントのテスト

require 'rails_helper'

RSpec.describe API::V1::Posts, type: :api do
  describe 'index' do
    before { send_request }
    
    let(:send_request) { get "/api/v1/posts", params.merge(format: :json) }
    let(:params) { {} }
    let(:user) { create(:user) } # FactoryGirlが入っている体でおねがいします
    let!(:posts) { create_list :post, rand(2..4), user: user }
    let(:search_result) { Post.latest }
    let(:expected) { render_api_response(search_result, with: API::V1::Entities::Post) }

    it do
      expect(response).to be_successful
      expect(response.body).to be_json_eql expected
    end
  end
end

まとめ

Grapeを使ったのは初めてだったのでベストプラクティスはわからないですが、今回のテスト方針はまあまあ機能したので個人的には悪くなかったんじゃないかなーと思っています。本当はOAuth2認可が必要なエンドポイントのテストとかもあって、そのあたりのことも書きたかったのですが、それはまた次の機会に。