184
Help us understand the problem. What are the problem?

More than 5 years have passed since last update.

posted at

updated at

Grape + RSpec + json_expressions で Awesome な API の受け入れテスト

Grape は Ruby で API を書くのに便利なフレームワークです。

Grape 自体については fakestarbaby 氏がすでにすてきなエントリを書いてくださっています。
Grape | API生成マイクロフレームワーク #Rails #Gems #Ruby #grape #api_builders - Qiita

ここではどうやってテストを書くのかということについて書いてみたいと思います。

想定

  • RSpec の受け入れテストの request_spec を使うよ
  • API は JSON を返すよ
  • API 用のサブドメイン(api.foobar.com)を切っているよ
  • JSON のテストは json_expressions を使うよ(参考
  • OAuth 2.0 の Provider になって Web Application Flow とかで認証しちゃったり
  • モックは FactoryGirl で作っちゃうよ

helper

まずは helper メソッドをいくつか api_helper.rb に定義します。

include Rack::Test::Methods

Rack::Test::Methods をインクルードすることで、get, post 等々を事項した結果のテストをおこなえるようになります。

spec/support/api_helper.rb
module ApiHelper
  include Rack::Test::Methods
end

API 用のサブドメインのための設定

API 用のサブドメインを切ったのでそれに応じるように、ヘルパーメソッドを定義しておきます。

テストの場合、ドメイン名は example.com になるようなので、サブドメインは api.example.com にしています。さきほど api.foobar.com と書いたのはこのときの api.example.com と混同しないようにするためです。

spec/support/api_helper.rb
module ApiHelper
  # (略)
  def domain
    'api.example.com'
  end

  def default_rack_env
    { 'HTTP_HOST' => domain }
  end
end

shared_context で全体の前提を定義!

spec/support/api_helper.rb
module ApiHelper
  # (略)
  shared_context 'api' do
    subject { eval("#{method}(url, parameters, rack_env)") }
    let(:parameters) { '' }
    let(:rack_env)   { default_rack_env }
  end
end

テスト用の request は HTTPメソッド名(url, parameters, rack_env) のようになるのですが、毎回書くのはかったるいので、subject に eval で押し込めました。

RSpec の subject を使うことで、it do end の中身は subject が実行された結果を受けるので、いきなり should から書き始めることができます。

HTTP の status と body を一気に試す shared_examples_for を定義!

spec/support/api_helper.rb
modulde ApiHelper
  # (略)
  shared_examples_for '200 Success' do
    its(:status) { should be(200) }
    its(:body)   { should match_json_expression(result) }
  end

  shared_examples_for '201 Created' do
    its(:status) { should be(201) }
    its(:body)   { should match_json_expression(result) }
  end

  shared_examples_for '204 No Content' do
    its(:status) { should be(204) }
    its(:body)   { should eq('') }
  end

  shared_examples_for '404 Not Found' do
    its(:status) { should be(404) }
    its(:body)   { should match_json_expression({ message: '404 Not Found' }) }
  end

  shared_examples_for '422 Unprocessable Entity' do
    its(:status) { should be(422) }
    its(:body)   { should match_json_expression({ message: '422 Unprocessable Entity' }) }
  end
end

先ほどの subject の結果で得られる last_response は status / body などのプロパティを持っているので、RSpec の its でさらに取り出してしまいます。

20x などの成功時の match_json_expression の中身は、使用時に let を使い result という名前で定義します。

OAuth 2 認証

OAuth 2 の認証が必要な場合は、先ほどの shared_context を拡張します。

spec/support/api_helper.rb
module ApiHelper
  # (略)
  def oauth2_auth(token)
    header('Authorization', "Bearer #{token}")
  end

  shared_context 'api' do
    let(:user)                { FactoryGirl.create(:user) }
    let(:client_application)  { FactoryGirl.create(:client_application,
                                                   user: user) }
    let(:access_token)        { FactoryGirl.create(:access_token,
                                                   user: user,
                                                   client_application: client_application) }

    before(:each) { oauth2_auth(access_token.token) }

    subject { eval("#{method}(url, parameters, rack_env)") }
    let(:parameters) { '' }
    let(:rack_env)   { default_rack_env }
  end
end

実際の使い方

ここまで作った helper を使って、実際にテストを書いてみましょう。

spec/requests/api/articles_spec.rb
describe Api do
  include ApiHelper
  include_context 'api'

  describe 'Articles' do
    let(:article) { FactoryGirl.create(:article, user: user) }

    describe 'GET /articles/:article_id' do
      let(:method) { 'get' }
      let(:url)    { "/articles/#{article.id}" }
      let(:result) do
        {
          id:      article.id,
          title:   article.title,
          content: article.content,
          user: {
            id:            user.id,
            name:          user.name,
            profile_image: user.profile_image.url(:small),
            created_at:    user.created_at.as_json,
            updated_at:    user.created_at.as_json,
          }
        }
      end

      it_behaves_like('200 Success')
    end

    describe 'DELETE /articles/:article_id' do
      let(:method) { 'delete' }
      let(:url)    { "/articles/#{article.id}" }

      it_behaves_like('204 No Content')

      it 'The article is successfully deleted.' do
        subject
        expect { Article.find(article.id) }.to raise_error ActiveRecord::RecordNotFound
      end
    end
  end
end

リクエスト時の parameter については必要があれば let は上書きができるので、上書きします。method や url は上の方で定義しておいて、context をネストさせれば、無駄なコードを書くことなくテストのコードの集中することが可能だと思います。

subject を it do の中で呼び出せば、response でない項目もテストが可能です。

全体として参考にしたもの

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
184
Help us understand the problem. What are the problem?