Edited at
VASILYDay 21

Swaggerで定義したAPIドキュメントとAPIレスポンスの差異をなくす

More than 1 year has passed since last update.


はじめに

VASILYでは、API開発を始める前に、Swaggerを用いてAPIドキュメントを作成しています。

APIドキュメントを作成した後、実際のAPIレスポンスを修正したがドキュメントの更新を忘れ、ドキュメントの定義と実際のレスポンスの間に差異が生じてしまうということがありました。

そこで、今回はcommitteeというgemを用いて、Swaggerで定義したAPIドキュメントと実際のAPIレスポンスとの差異を検知する方法をご紹介します。


committeeとは

committeeは、実際のAPIリクエストやレスポンスがスキーマ定義にそっているかをチェックすることができるgemです。

Rackのミドルウェアとして動作します。

バージョン2.0からはJSON Schemaだけでなく、OpenAPI2.0(Swagger)もサポートされるようになったため今回はこちらのgemを使用します。


committeeのインストール

Gemfileに追記します。

gem 'committee'


APIドキュメンテーションを用意する


Swaggerを書く

SwaggerでAPIドキュメントを記述します。

Swaggerの導入方法、書き方については、下記の記事を参考にして下さい。

開発効率を上げる!Swaggerで作るWEB APIモック

Swaggerの記法まとめ

SwaggerはYAMLとJSONで書くことができます。

今回は、YAMLを採用し、公式ドキュメントでサンプルとして使われているPetstoreAPIを簡略化したものをサンプルAPIドキュメントとして用います。

# pet.yaml

swagger: "2.0"
info:
description: "これはペットストアに関するAPIです。"
version: "1.0.0"
title: "Petstore API"
termsOfService: "http://swagger.io/terms/"
contact:
email: "apiteam@swagger.io"
license:
name: "Apache 2.0"
url: "http://www.apache.org/licenses/LICENSE-2.0.html"
produces:
- "application/json"
consumes:
- application/x-www-form-urlencoded
paths:
/pets/{petId}:
get:
summary: "ペット情報API"
description: "指定されたpetIdの情報を返します"
operationId: 'get_pet_by_id'
tags:
- 'pet'
parameters:
- name: "petId"
in: "path"
description: "取得したいペットのID"
required: true
type: "integer"
responses:
200:
description: "成功時のレスポンス"
schema:
$ref: '#/definitions/Pet'

definitions:
Pet:
type: object
required:
- id
- name
properties:
id:
type: "integer"
name:
type: "string"
example: "doggie"


YAMLをJSONに変換

YAMLで書いたSwaggerをJSONに変換します。

変換には、swagger-codegenを使用します。

swagger-codegenは、Swaggerで書かれたスキーマ定義からクライアントやサーバーのコードを生成するツールです。

多くの言語やフレームワークに対応しており、Rubyのモックコードも生成できます。

swagger-codegenを使用してYAMLをJSONに変換します。

今回はMacのローカルで生成を行うのでhomebrewでswagger-codegenを入れます。

# swagger-codegenをインストール

% brew install swagger-codegen

# yamlからjsonへ変換
% swagger-codegen generate -i pet.yaml -l swagger

作成されたJSONが下記になります。

{

"swagger" : "2.0",
"info" : {
"description" : "これはペットストアに関するAPIです。",
"version" : "1.0.0",
"title" : "Petstore API",
"termsOfService" : "http://swagger.io/terms/",
"contact" : {
"email" : "apiteam@swagger.io"
},
"license" : {
"name" : "Apache 2.0",
"url" : "http://www.apache.org/licenses/LICENSE-2.0.html"
}
},
"consumes" : [ "application/x-www-form-urlencoded" ],
"produces" : [ "application/json" ],
"paths" : {
"/pets/{petId}" : {
"get" : {
"tags" : [ "pet" ],
"summary" : "ペット情報API",
"description" : "指定されたpetIdの情報を返します",
"operationId" : "get_pet_by_id",
"parameters" : [ {
"name" : "petId",
"in" : "path",
"description" : "取得したいペットのID",
"required" : true,
"type" : "integer"
} ],
"responses" : {
"200" : {
"description" : "成功時のレスポンス",
"schema" : {
"$ref" : "#/definitions/Pet"
}
}
}
}
}
},
"definitions" : {
"Pet" : {
"type" : "object",
"required" : [ "id", "name" ],
"properties" : {
"id" : {
"type" : "integer"
},
"name" : {
"type" : "string",
"example" : "doggie"
}
}
}
}
}


request specを書く

APIドキュメントにもとづいたAPIを作成します。

今回はわかりやすくするために、swagger-codegenを使ってモックコードを生成せずに、手動でRailsのコードを作成します。

config/routes.rb に/pets/#{pet_id}のルーティングを追加し、JSONを返すだけのControllerを作成します。

# config/routes.rb

resources :pets, only: [:show]

# app/controllers/pets_controller.rb

class PetsController < ApplicationController
def show
render json: { id: 1, name: 'pochi' }
end
end

APIのレスポンスをテストするために、request specを書きます。

request specは、クライアント側の動作や振る舞い、特定のリクエストに対するHTTPレスポンスをテストします。

詳しい説明については、rspecのREADMEを参考にしてください。

PetstoreAPIのrequest specを作成します。

% bundle exec rails g rspec:integration Pet

上記のコマンドを実行すると、下記のようなspecが生成されます。

# spec/requests/pets_spec.rb

require 'rails_helper'

RSpec.describe "Pets", type: :request do
describe "GET /pets" do
it "works! (now write some real specs)" do
get pets_path
expect(response).to have_http_status(200)
end
end
end

このrequest specに、committeeの設定を追記します。

今回はわかりやすさを重視して、specファイル内にcommitteeの設定を書きます。

committeeの設定を追加すると下記のようになります。

# spec/requests/pet_spec.rb

require 'rails_helper'

RSpec.describe "Pets", type: :request do
include Committee::Test::Methods
include Rack::Test::Methods

def committee_schema
@committee_schema ||=
begin
driver = Committee::Drivers::OpenAPI2.new
schema = JSON.parse(File.read(schema_path))
driver.parse(schema)
end
end

def schema_path
Rails.root.join('swagger.json')
end

describe "GET /pets", type: :request do
it "レスポンスがAPIドキュメントと一致する" do
get '/pets/1'
assert_schema_conform
end
end
end

schema_pathメソッドとcommittee_schemaメソッドを追加しました。

どちらも、committeeのメソッドをオーバーライドしています。

schema_pathは、API定義のJSONが置かれているパスを記述します。

committeeのschema_pathは、使用する際にオーバライドする必要があり、オーバーライドしないとエラーが返ります。

schema_path

committee_schemaは、OpenAPI Specをパースするためにオーバーライドします。

このメソッドはAPIドキュメントを読み込む際に使用されます。

driverを指定しないと、JSON Hyper Schemaのドライバが使われます。

今回は、Swaggerで定義したOpenAPI Specをパースするために、OpenAPIのドライバを使用したいので、committee_schemaメソッドをオーバーライドします。

committee_schema

  def committee_schema

@committee_schema ||=
begin
driver = Committee::Drivers::OpenAPI2.new
schema = JSON.parse(File.read(schema_path))
driver.parse(schema)
end
end

テスト内にassert_schema_confirmを記述することで、APIドキュメントと実際のレスポンスが、一致しているかどうかのテストが実行されます。

APIドキュメント内で、requiredに記述した必須カラムを記述していない場合は、下記のようなエラーが表示されます。

 1) Pets GET /pets レスポンスがAPIドキュメントと一致する

Failure/Error: assert_schema_conform

Committee::InvalidResponse:
Invalid response.

#: failed schema #/properties//pets/{petId}/properties/GET: "name" wasn't supplied.
# ./spec/requests/pets_spec.rb:22:in `block (3 levels) in <top (required)>'


最後に

Swaggerと実際のAPIレスポンスを、人が都度修正する運用は辛いですし、どうしても差異が発生しやすくなります。

committeeとrequest specを用いて、ドキュメントとの差異を防いでいきたいです。


参考

スキーマファースト開発のススメ

Web APIのレスポンスJSONをCommittee + OpenAPIでバリデーションして仕様と実装の乖離を防ぐ

JSON Validation by Committee