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
使えばよさそう - Rspec で
type: :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 認可が必要なエンドポイントのテストとかもあって、そのあたりのことも書きたかったのですが、それはまた次の機会に。