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

Rails Webook

自社のECを開発している会社で働いています。Rails情報やサービスを成長させる方法を書いていきます

RailsでAPI作成とAPIのテストのまとめ

テスト Rails API Rails中級

f:id:nipe880324:20150108014534j:plain:w560
Photo by Gonzalo Baeza | Flickr - Photo Sharing!


RailsJSONを返すAPIを作成し、また、APIのテスト方法も説明します。
JSONを返すAPIは、RailsActiveSupportより拡張されたto_jsonメソッドとDMMが開発したjbuilderというGemを使います。
APIのテストにはおなじみのRSpec3を使います。



動作確認

目次

3. APIのテスト

3.1. テストファイルの準備
3.2. 一覧(index)APIのテスト
3.3. 詳細(show)APIのテスト
3.4. 作成(create)/更新(update)APIのテスト
3.5. 削除(destroy)APIのテスト


1. 前提条件

次のコントローラー、モデル、ビューを前提に話を進めていきます。

rails g scaffold Product name:string price:integer category_id:integer
rails g model Category name

# app/models/category.rb
class Category < ActiveRecord::Base
  has_many :products
end

# app/models/product.rb
class Product < ActiveRecord::Base
  belongs_to :category
end


2. APIの作成

Railsでは、ActiveRecordto_jsonメソッドが定義されているので基本的にはこれを使えばJSONを返すことが可能です。
URLや関連先の値もJSON形式で返したい場合は、jbuilderというgemをつかいます。


2.1. 1つのコントローラーでHTMLやJSONを返すAPI

1つのコントローラーでHTMLやJSONを返すには、コントローラーでrespond_toメソッドを使います。

まず、通常通りresoucesでルーティングを設定します。

Rails.application.routes.draw do
  resources :products
end


# ルーティング結果
$ bin/rake routes
      Prefix Verb   URI Pattern                  Controller#Action
    products GET    /products(.:format)          products#index
             POST   /products(.:format)          products#create
 new_product GET    /products/new(.:format)      products#new
edit_product GET    /products/:id/edit(.:format) products#edit
     product GET    /products/:id(.:format)      products#show
             PATCH  /products/:id(.:format)      products#update
             PUT    /products/:id(.:format)      products#update
             DELETE /products/:id(.:format)      products#destroy


その後、コントローラー内でrespond_toを使い、フォーマットに応じたレスポンスを返します。

class Api::ProductsController < ApplicationController
  def index
    @products = Product.all
    respond_to do |format|
      format.html # => 通常のURLの場合、index.html.erb が返される
      format.json { render json: @products } # URLが.jsonの場合、@products.to_json が返される
    end
  end

  ...
end

デフォルトのフォーマットは、htmlであり、/products.jsonなどURLの最後に.jsonをつければformat.jsonのブロック内の値が返されます。
format.jsonのブロック内では、renderメソッドにより、@productsの値がJSON形式で返されます。(内部的には、@products.to_jsonメソッドが呼ばれます)



to_jsonメソッドは、簡易にJSON形式に変換できるので便利なメソッドですが、複数のモデルが絡んだり、URLを返すなど複雑なJSONを返す場合は扱いずらいです。
そのため、jbuilderを使い、返したいJSONの内容を、ビューファイルに記載します。

# app/views/products/index.json.jbuilder
json.array!(@products) do |product|
  json.extract! product, :id, :name, :price, :publised_at, :category_id
  json.url product_url(product, format: :json)
end

コントローラーでは、format.jsonを修正し、jbuilderのindexファイルを呼ぶようにします。

respond_to do |format|
  format.html # app/views/products/index.html.erb
  format.json # app/views/products/index.json.jbuilder
end


そして、ブラウザから/products.jsonへアクセスすると次のようにjbuilderで定義したJSONが返されます。
f:id:nipe880324:20150103220854p:plain


jbuilderの細かな使い方は、jbuilder - GitHub公式ページを確認してください。



2.2. JSONのみを返すAPI

JSONのみを返すAPIを作成する方法を説明します。
まず、ルーティング設定でnamespace :api, { format: 'json' }に囲みます。

# config/routes.rb
Rails.application.routes.draw do
  namespace :api, { format: 'json' } do
    resources :products
  end

  resources :products
  root to: 'products#index'
end



# ルーティング結果
$ rake routes
          Prefix Verb   URI Pattern                      Controller#Action
    api_products GET    /api/products(.:format)          api/products#index {:format=>"json"}
                 POST   /api/products(.:format)          api/products#create {:format=>"json"}
 new_api_product GET    /api/products/new(.:format)      api/products#new {:format=>"json"}
edit_api_product GET    /api/products/:id/edit(.:format) api/products#edit {:format=>"json"}
     api_product GET    /api/products/:id(.:format)      api/products#show {:format=>"json"}
                 PATCH  /api/products/:id(.:format)      api/products#update {:format=>"json"}
                 PUT    /api/products/:id(.:format)      api/products#update {:format=>"json"}
                 DELETE /api/products/:id(.:format)      api/products#destroy {:format=>"json"}
                 .....

このようにネームスペースを通常のコントローラーとAPIのコントローラーで分けるのが一般的です。
また、デフォルトのフォーマットをjsonに定義しているので、/productsのようにURLの最後に.jsonをつけない場合でも、JSON形式でレスポンスを返すようになります。


次に、コントローラーを定義します。ネームスペースをつけたので、apiフォルダ配下に作成します。
また、Apiモジュール内にコントローラーを定義します。

# app/controllers/api/products_controller.rb
module Api
  class ProductsController < ApplicationController

    def index
      @products = Product.all
      render json: @products
    end
  end

  ...
end


2.3. APIのバージョニング

APIを外部に公開する場合は、v1やv2などバージョニングすることが必要です。

次のようにnamespace :v1namespace :v2を追加することでバージョニングを行えます。

# config/routes.rb
Store::Applicaition.routes.draw do
  namespace :api, { format: 'json' } do
    namespace :v1 do 
      resources :products
    end
    namespace :v2 do
      resources :products
      # v2のリソース宣言 ...
    end
  end

  resouces :products
  root to: 'products#index'
end



# ルーティング結果
$ rake routes
             Prefix Verb   URI Pattern                         Controller#Action
    api_v1_products GET    /api/v1/products(.:format)          api/v1/products#index {:format=>"json"}
    ....
    api_v2_products GET    /api/v2/products(.:format)          api/v2/products#index {:format=>"json"}
    ....

コントローラーもネームスペースに合わせてmodule V1を追加する必要があります。

# app/controllers/api/v1/products_controller.rb
module Api
  module V1
    class ProductsController < ApplicationController

      def index
        @products = Product.all
        render json: @products
      end
    end

    ...
  end
end



3. APIのテスト

3.1. テストファイルの準備

APIのテストは、spec/requestsを使います。
これは、spec/controllersでは、コントローラーのみでコントローラー(API)からの返り値(JSONなど)を検証するのが大変であり、spec/featuresでは、CapybaraやPoltergistを起動させてテスト実行時間を無駄に長くしてしまうためです。

次のコマンドでrequestsファイルを作成します。

rails g rspec:integration Product
   identical  spec/requests/products_spec.rb

3.2. 一覧(index)APIのテスト

一覧APIのテストは、「ステータスコード」と「結果の件数」を確認する。
詳細APIのテストと重複している場合は、詳細な値の確認は省略してもよい。

テスト対象(一覧API

# コントローラー
# app/controllers/products_controller.rb
class ProductsController < ApplicationController
  # GET /products
  # GET /products.json
  def index
    @products = Product.all
  end
end

# index.json.jbuilder
json.array!(@products) do |product|
  json.extract! product, :id, :name, :price
  json.url product_url(product, format: :json)
end


ストファイル(一覧API

# spec/requests/products_spec.rb
require 'rails_helper'

RSpec.describe "Products", :type => :request do

  describe "GET /products.json" do
    before { @products = FactoryGirl.create_list(:product, 2) }

    it "一覧情報を取得できること" do
      # GET /products.json にアクセスする
      get products_path format: :json

      # ステータスコードの確認
      expect(response.status).to eq 200

      # JSONの確認
      json = JSON.parse(response.body)
      expect(json.size).to     eq @products.count
      expect(json[0]["id"]).to eq @products[0].id
      expect(json[1]["id"]).to eq @products[1].id

      # 詳細の値の確認は省略
    end
  end
end


3.3. 詳細(show)APIのテスト

詳細APIのテストは「各項目が取得できていること」を確認します。

テスト対象(詳細API)

# コントローラー
# app/controllers/products_controller.rb
class ProductsController < ApplicationController
  # GET /product/1
  # GET /product/1.json
  def show
    @product = Product.find(params[:id])
  end
end

# show.json.jbuilder
json.extract! @product, :id, :name, :price, :publised_at, :category_id, :created_at, :updated_at


ストファイル(詳細API)

# spec/requests/products_spec.rb
require 'rails_helper'

RSpec.describe "Products", :type => :request do

  describe "GET /product/:id.json" do
    before { @product = FactoryGirl.create(:product) }

    it "詳細情報を取得できること" do
      # GET /product/:id.json にアクセスする
      get product_path(@product.id, format: :json)

      # ステータスコードの確認
      expect(response.status).to eq 200

      # JSONの各項目の確認
      json = JSON.parse(response.body)
      expect(json["id"]).to    eq @product.id
      expect(json["name"]).to  eq @product.name
      expect(json["price"]).to eq @product.price
      # ... その他の項目
    end
  end
end

3.4. 作成(create)/更新(update)APIのテスト

作成/更新APIのテストは「作成/更新できていること」を確認します。
また、エラー時には「エラーコードが返ってくること」を確認します。


テスト対象ファイル(作成API)

# コントローラー
# app/controllers/products_controller.rb
class ProductsController < ApplicationController
    # POST /products
    # POST /products.json
    def create
      @product = Product.new(product_params)

      respond_to do |format|
        if @product.save
          format.html { redirect_to @product, notice: 'Product was successfully created.' }
          format.json { render :show, status: :created, location: @product }
        else
          format.html { render :new }
          format.json { render json: @product.errors, status: :unprocessable_entity }
        end
      end
    end
  end

  private
    def product_params
      params.require(:product).permit(:name, :price, :publised_at, :category_id)
    end
end

# show.json.jbuilder
json.extract! @product, :id, :name, :price, :publised_at, :category_id, :created_at, :updated_at


ストファイル(作成API)
※更新APIは作成APIとほぼ同様のため省略します。

# spec/requests/products_spec.rb
require 'rails_helper'

RSpec.describe "Products", :type => :request do

  describe "POST /products.json" do
    it "商品情報が作成されること" do
      params = { product: FactoryGirl.attributes_for(:product) }
      # => {:product=>{:name=>"MyString", :price=>1, :publised_at=>"2014-12-29 23:40:30", :category_id=>1}}

      # 商品数が1増えることを確認
      expect {
        # POST /products.json にアクセスする
        post products_path(format: :json), params
      }.to change { Product.count }.by(1)

      # ステータスコードの確認
      expect(response.status).to eq 201

      # JSONの各値の確認
      json = JSON.parse(response.body)
      expect(json["name"]).to  eq "MyString"
      expect(json["price"]).to eq 1
      # ... その他のカラム

      # locationが作成したProductの詳細画面のURLであることを確認
      expect(response.location).to eq product_url(Product.last)
    end

    it "商品情報が作成されないこと" do
      # バリデーションエラーなどで作成されないようにし、帰り値を確認する
    end
  end
end

3.5. 削除(destroy)APIのテスト

削除APIのテストは「削除されていること」を確認します。

テスト対象ファイル(削除API)

# コントローラー
# app/controllers/products_controller.rb
class ProductsController < ApplicationController
  # DELETE /products/1
  # DELETE /products/1.json
  def destroy
    @product = Product.find(params[:id])
    @product.destroy
    respond_to do |format|
      format.html { redirect_to products_url, notice: 'Product was successfully destroyed.' }
      format.json { head :no_content }
    end
  end
end


ストファイル(削除API)
※更新APIは作成APIとほぼ同様のため省略します。

# spec/requests/products_spec.rb
require 'rails_helper'

RSpec.describe "Products", :type => :request do

  describe "DELETE /products/:id.json" do
    before { @product = FactoryGirl.create(:product) }

    it "商品情報が削除されること" do
      # Productの数が-1されること
      expect {
        # DELETE /products/:id.json にアクセスする
        delete product_path(@product.id, format: :json)
      }.to change { Product.count }.by(-1)

      # ステータスコードの確認
      expect(response.status).to eq 204
    end
  end
end


以上です。