From 9ad7a49e6dd4b963f466d84e12c91d50e4dbafb9 Mon Sep 17 00:00:00 2001 From: domaindrivendev Date: Tue, 23 Feb 2016 00:30:10 -0800 Subject: [PATCH] assert response body based on spec examples --- Gemfile | 1 + Gemfile.lock | 2 + ...t_data_builder.rb => test_case_builder.rb} | 53 ++++++++++++----- lib/swagger_rails/testing/test_visitor.rb | 7 ++- .../dummy/app/controllers/blogs_controller.rb | 14 ++++- spec/dummy/app/models/blog.rb | 10 ++++ .../app/views/layouts/application.html.erb | 14 ----- spec/dummy/config/application.rb | 2 +- spec/dummy/config/database.yml | 25 ++++++++ spec/dummy/config/routes.rb | 4 +- spec/dummy/config/swagger/v1/swagger.json | 9 ++- .../db/migrate/20160218212104_create_blogs.rb | 10 ++++ spec/dummy/db/schema.rb | 23 ++++++++ .../test/integration/v1_contract_test.rb | 11 +++- ...lder_spec.rb => test_case_builder_spec.rb} | 58 ++++++++++++++++--- spec/testing/test_visitor_spec.rb | 34 ++++++++--- 16 files changed, 218 insertions(+), 59 deletions(-) rename lib/swagger_rails/testing/{test_data_builder.rb => test_case_builder.rb} (56%) create mode 100644 spec/dummy/app/models/blog.rb delete mode 100644 spec/dummy/app/views/layouts/application.html.erb create mode 100644 spec/dummy/config/database.yml create mode 100644 spec/dummy/db/migrate/20160218212104_create_blogs.rb create mode 100644 spec/dummy/db/schema.rb rename spec/testing/{test_data_builder_spec.rb => test_case_builder_spec.rb} (57%) diff --git a/Gemfile b/Gemfile index d4c5bc7..670cece 100644 --- a/Gemfile +++ b/Gemfile @@ -13,6 +13,7 @@ gemspec # To use a debugger # gem 'debugger', group: [:development, :test] +gem 'sqlite3' gem 'pry' gem 'rspec-rails' gem 'generator_spec' diff --git a/Gemfile.lock b/Gemfile.lock index 9863a5b..a957712 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -105,6 +105,7 @@ GEM multi_json (~> 1.0) rack (~> 1.0) tilt (~> 1.1, != 1.3.0) + sqlite3 (1.3.11) thor (0.19.1) tilt (1.4.1) treetop (1.4.15) @@ -119,4 +120,5 @@ DEPENDENCIES generator_spec pry rspec-rails + sqlite3 swagger_rails! diff --git a/lib/swagger_rails/testing/test_data_builder.rb b/lib/swagger_rails/testing/test_case_builder.rb similarity index 56% rename from lib/swagger_rails/testing/test_data_builder.rb rename to lib/swagger_rails/testing/test_case_builder.rb index 0433475..4336a6a 100644 --- a/lib/swagger_rails/testing/test_data_builder.rb +++ b/lib/swagger_rails/testing/test_case_builder.rb @@ -1,13 +1,12 @@ module SwaggerRails - - class TestDataBuilder + + class TestCaseBuilder def initialize(path_template, http_method, swagger) @path_template = path_template @http_method = http_method @swagger = swagger @param_values = {} - @expected_status = nil end def set(param_values) @@ -15,7 +14,7 @@ module SwaggerRails end def expect(status) - @expected_status = status + @expected_status = status.to_s end def test_data @@ -33,7 +32,7 @@ module SwaggerRails private def find_operation! - keys = [ 'paths', @path_template, @http_method ] + keys = [ 'paths', @path_template, @http_method ] operation = find_hash_item!(@swagger, keys) operation || (raise MetadataError.new(keys)) end @@ -52,35 +51,59 @@ module SwaggerRails end def build_params(parameters) - {}.tap do |params| - params.merge!(param_values_for(parameters, 'query')) - body_param_values = param_values_for(parameters, 'body') - params.merge!(body_param_values.values.first) if body_param_values.any? - end + body_param_values = param_values_for(parameters, 'body') + return body_param_values.values.first.to_json if body_param_values.any? + param_values_for(parameters, 'query') end def build_headers(parameters) param_values_for(parameters, 'header') + .merge({ + 'CONTENT_TYPE' => 'application/json', + 'ACCEPT' => 'application/json' + }) end def build_expected_response(responses) + status = @expected_status || responses.keys.find { |k| k.start_with?('2') } + response = responses[status] || (raise MetadataError.new('paths', @path_template, @http_method, 'responses', status)) + { + status: status.to_i, + body: response_body_for(response) + } end def param_values_for(parameters, location) applicable_parameters = parameters.select { |p| p['in'] == location } - Hash[applicable_parameters.map { |p| [ p['name'], value_for(p) ] }] + Hash[applicable_parameters.map { |p| [ p['name'], param_value_for(p) ] }] end - def value_for(param) - return @param_values[param['name']] if @param_values.has_key?(param['name']) - return param['default'] unless param['in'] == 'body' - schema_for(param['schema'])['example'] + def param_value_for(parameter) + return @param_values[parameter['name']] if @param_values.has_key?(parameter['name']) + return parameter['default'] unless parameter['in'] == 'body' + schema = schema_for(parameter['schema']) + schema_example_for(schema) + end + + def response_body_for(response) + return nil if response['schema'].nil? + schema = schema_for(response['schema']) + schema_example_for(schema) end def schema_for(schema_or_ref) return schema_or_ref if schema_or_ref['$ref'].nil? @swagger['definitions'][schema_or_ref['$ref'].sub('#/definitions/', '')] end + + def schema_example_for(schema) + return schema['example'] if schema['example'].present? + # If an array, try construct from the item example + if schema['type'] == 'array' && schema['item'].present? + item_schema = schema_for(schema['item']) + return [ schema_example_for(item_schema) ] + end + end end class MetadataError < StandardError diff --git a/lib/swagger_rails/testing/test_visitor.rb b/lib/swagger_rails/testing/test_visitor.rb index f024b5a..9603e1c 100644 --- a/lib/swagger_rails/testing/test_visitor.rb +++ b/lib/swagger_rails/testing/test_visitor.rb @@ -1,4 +1,4 @@ -require 'swagger_rails/testing/test_data_builder' +require 'swagger_rails/testing/test_case_builder' module SwaggerRails @@ -9,7 +9,7 @@ module SwaggerRails end def run_test(path_template, http_method, test, &block) - builder = TestDataBuilder.new(path_template, http_method, @swagger) + builder = TestCaseBuilder.new(path_template, http_method, @swagger) builder.instance_exec(&block) if block_given? test_data = builder.test_data @@ -19,7 +19,8 @@ module SwaggerRails test_data[:headers] ) - test.assert_response(test_data[:expected_status]) + test.assert_response(test_data[:expected_response][:status]) + test.assert_equal(test_data[:expected_response][:body], JSON.parse(test.response.body)) end end end diff --git a/spec/dummy/app/controllers/blogs_controller.rb b/spec/dummy/app/controllers/blogs_controller.rb index ea51be9..a36145a 100644 --- a/spec/dummy/app/controllers/blogs_controller.rb +++ b/spec/dummy/app/controllers/blogs_controller.rb @@ -1,14 +1,22 @@ class BlogsController < ApplicationController + wrap_parameters Blog + respond_to :json + # POST /blogs def create - render json: {} + @blog = Blog.create(params[:blog]) + respond_with @blog end + # GET /blogs def index - render json: {} + @blogs = Blog.all + respond_with @blogs end + # GET /blogs/1 def show - render json: {} + @blog = Blog.find(params[:id]) + respond_with @blog end end diff --git a/spec/dummy/app/models/blog.rb b/spec/dummy/app/models/blog.rb new file mode 100644 index 0000000..2f4b8f8 --- /dev/null +++ b/spec/dummy/app/models/blog.rb @@ -0,0 +1,10 @@ +class Blog < ActiveRecord::Base + attr_accessible :content, :title + + def as_json(options) + { + title: title, + content: content + } + end +end diff --git a/spec/dummy/app/views/layouts/application.html.erb b/spec/dummy/app/views/layouts/application.html.erb deleted file mode 100644 index 593a778..0000000 --- a/spec/dummy/app/views/layouts/application.html.erb +++ /dev/null @@ -1,14 +0,0 @@ - - - - Dummy - <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> - <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> - <%= csrf_meta_tags %> - - - -<%= yield %> - - - diff --git a/spec/dummy/config/application.rb b/spec/dummy/config/application.rb index f0d61fd..11d35dc 100644 --- a/spec/dummy/config/application.rb +++ b/spec/dummy/config/application.rb @@ -1,7 +1,7 @@ require File.expand_path('../boot', __FILE__) # Pick the frameworks you want: -# require "active_record/railtie" +require "active_record/railtie" require "action_controller/railtie" require "action_mailer/railtie" require "action_view/railtie" diff --git a/spec/dummy/config/database.yml b/spec/dummy/config/database.yml new file mode 100644 index 0000000..dc954e3 --- /dev/null +++ b/spec/dummy/config/database.yml @@ -0,0 +1,25 @@ +#SQLite version 3.x +# gem install sqlite3 +# +# Ensure the SQLite 3 gem is defined in your Gemfile +# gem 'sqlite3' +development: + adapter: sqlite3 + database: db/development.sqlite3 + pool: 5 + timeout: 5000 + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + adapter: sqlite3 + database: db/test.sqlite3 + pool: 5 + timeout: 5000 + +production: + adapter: sqlite3 + database: db/production.sqlite3 + pool: 5 + timeout: 5000 diff --git a/spec/dummy/config/routes.rb b/spec/dummy/config/routes.rb index 2636b39..99850bd 100644 --- a/spec/dummy/config/routes.rb +++ b/spec/dummy/config/routes.rb @@ -1,5 +1,5 @@ Rails.application.routes.draw do - mount SwaggerRails::Engine => '/api-docs' + resources :blogs, defaults: { :format => :json } - resources :blogs, only: [ :create, :index, :show ] + mount SwaggerRails::Engine => '/api-docs' end diff --git a/spec/dummy/config/swagger/v1/swagger.json b/spec/dummy/config/swagger/v1/swagger.json index d5584c7..8b859d9 100644 --- a/spec/dummy/config/swagger/v1/swagger.json +++ b/spec/dummy/config/swagger/v1/swagger.json @@ -24,8 +24,8 @@ } ], "responses": { - "200": { - "description": "Ok", + "201": { + "description": "Created", "schema": { "$ref": "#/definitions/Blog" } @@ -58,7 +58,10 @@ "200": { "description": "Ok", "schema": { - "$ref": "#/definitions/Blog" + "type": "array", + "item": { + "$ref": "#/definitions/Blog" + } } } } diff --git a/spec/dummy/db/migrate/20160218212104_create_blogs.rb b/spec/dummy/db/migrate/20160218212104_create_blogs.rb new file mode 100644 index 0000000..81d9813 --- /dev/null +++ b/spec/dummy/db/migrate/20160218212104_create_blogs.rb @@ -0,0 +1,10 @@ +class CreateBlogs < ActiveRecord::Migration + def change + create_table :blogs do |t| + t.string :title + t.text :content + + t.timestamps + end + end +end diff --git a/spec/dummy/db/schema.rb b/spec/dummy/db/schema.rb new file mode 100644 index 0000000..082bb8d --- /dev/null +++ b/spec/dummy/db/schema.rb @@ -0,0 +1,23 @@ +# encoding: UTF-8 +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# Note that this schema.rb definition is the authoritative source for your +# database schema. If you need to create the application database on another +# system, you should be using db:schema:load, not running all the migrations +# from scratch. The latter is a flawed and unsustainable approach (the more migrations +# you'll amass, the slower it'll run and the greater likelihood for issues). +# +# It's strongly recommended to check this file into your version control system. + +ActiveRecord::Schema.define(:version => 20160218212104) do + + create_table "blogs", :force => true do |t| + t.string "title" + t.text "content" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + +end diff --git a/spec/dummy/test/integration/v1_contract_test.rb b/spec/dummy/test/integration/v1_contract_test.rb index 01f3fc2..586d94b 100644 --- a/spec/dummy/test/integration/v1_contract_test.rb +++ b/spec/dummy/test/integration/v1_contract_test.rb @@ -5,5 +5,14 @@ class V1ContractTest < ActionDispatch::IntegrationTest include SwaggerRails::TestHelpers swagger_doc 'v1/swagger.json' - swagger_test_all +# +# test '/blogs post' do +# swagger_test '/blogs', 'post' +# end + + test '/blogs get' do + blog = Blog.create(title: 'Test Blog', content: 'Hello World') + + swagger_test '/blogs', 'get' + end end diff --git a/spec/testing/test_data_builder_spec.rb b/spec/testing/test_case_builder_spec.rb similarity index 57% rename from spec/testing/test_data_builder_spec.rb rename to spec/testing/test_case_builder_spec.rb index f403107..d5f7404 100644 --- a/spec/testing/test_data_builder_spec.rb +++ b/spec/testing/test_case_builder_spec.rb @@ -1,9 +1,9 @@ require 'rails_helper' -require 'swagger_rails/testing/test_data_builder' +require 'swagger_rails/testing/test_case_builder' module SwaggerRails - describe TestDataBuilder do + describe TestCaseBuilder do subject { described_class.new(path, method, swagger) } let(:swagger) do file_path = File.join(Rails.root, 'config/swagger', 'v1/swagger.json') @@ -59,20 +59,20 @@ module SwaggerRails end end - context 'operation has body params' do + context 'operation has body param' do let(:path) { '/blogs' } let(:method) { 'post' } context 'by default' do - it "includes params built from 'default' values" do - expect(test_data[:params]).to eq({ 'title' => 'Test Blog', 'content' => 'Hello World' }) + it "includes params string based on schema 'example'" do + expect(test_data[:params]).to eq({ 'title' => 'Test Blog', 'content' => 'Hello World' }.to_json) end end context 'values explicitly set' do before { subject.set blog: { 'title' => 'foobar' } } - it 'includes params build from set values' do - expect(test_data[:params]).to eq({ 'title' => 'foobar' }) + it 'includes params string based on set value' do + expect(test_data[:params]).to eq({ 'title' => 'foobar' }.to_json) end end end @@ -83,14 +83,54 @@ module SwaggerRails context 'by default' do it "includes headers built from 'default' values" do - expect(test_data[:headers]).to eq({ 'X-Forwarded-For' => 'client1' }) + expect(test_data[:headers]).to eq({ + 'X-Forwarded-For' => 'client1', + 'CONTENT_TYPE' => 'application/json', + 'ACCEPT' => 'application/json' + }) end end context 'values explicitly params' do before { subject.set 'X-Forwarded-For' => '192.168.1.1' } it 'includes headers built from set values' do - expect(test_data[:headers]).to eq({ 'X-Forwarded-For' => '192.168.1.1' }) + expect(test_data[:headers]).to eq({ + 'X-Forwarded-For' => '192.168.1.1', + 'CONTENT_TYPE' => 'application/json', + 'ACCEPT' => 'application/json' + }) + end + end + end + + context 'operation returns an object' do + let(:path) { '/blogs' } + let(:method) { 'post' } + + context 'by default' do + it "includes expected_response based on spec'd 2xx status" do + expect(test_data[:expected_response][:status]).to eq(201) + expect(test_data[:expected_response][:body]).to eq({ 'title' => 'Test Blog', 'content' => 'Hello World' }) + end + end + + context 'expected status explicitly set' do + before { subject.expect 400 } + it "includes expected_response based on set status" do + expect(test_data[:expected_response][:status]).to eq(400) + expect(test_data[:expected_response][:body]).to eq({ 'title' => [ 'is required' ] }) + end + end + end + + context 'operation returns an array' do + let(:path) { '/blogs' } + let(:method) { 'get' } + + context 'by default' do + it "includes expected_response based on spec'd 2xx status" do + expect(test_data[:expected_response][:status]).to eq(200) + expect(test_data[:expected_response][:body]).to eq([ { 'title' => 'Test Blog', 'content' => 'Hello World' } ]) end end end diff --git a/spec/testing/test_visitor_spec.rb b/spec/testing/test_visitor_spec.rb index 18ce6ef..7409fe9 100644 --- a/spec/testing/test_visitor_spec.rb +++ b/spec/testing/test_visitor_spec.rb @@ -12,19 +12,30 @@ module SwaggerRails let(:test) { spy('test') } describe '#run_test' do + before do + allow(test).to receive(:response).and_return(OpenStruct.new(body: "{}")) + end + context 'by default' do before { subject.run_test('/blogs', 'post', test) } it "submits request based on 'default' and 'example' param values" do expect(test).to have_received(:post).with( '/blogs', - { 'title' => 'Test Blog', 'content' => 'Hello World' }, - { 'X-Forwarded-For' => 'client1' } + { 'title' => 'Test Blog', 'content' => 'Hello World' }.to_json, + { 'X-Forwarded-For' => 'client1', 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' } ) end - it "asserts response matches first 2xx status in operation 'responses'" do - expect(test).to have_received(:assert_response).with(200) + it "asserts response matches spec'd 2xx status" do + expect(test).to have_received(:assert_response).with(201) + end + + it "asserts response body matches schema 'example' for 2xx status" do + expect(test).to have_received(:assert_equal).with( + { 'title' => 'Test Blog', 'content' => 'Hello World' }, + {} + ) end end @@ -39,22 +50,29 @@ module SwaggerRails it 'submits a request based on provided param values' do expect(test).to have_received(:post).with( '/blogs', - { 'title' => 'foobar' }, - { 'X-Forwarded-For' => '192.168.1.1' } + { 'title' => 'foobar' }.to_json, + { 'X-Forwarded-For' => '192.168.1.1', 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' } ) end end - context 'expected status explicitly params' do + context 'expected status explicitly set' do before do subject.run_test('/blogs', 'post', test) do expect 400 end end - it "asserts response matches params status" do + it "asserts response matches set status" do expect(test).to have_received(:assert_response).with(400) end + + it "asserts response body matches schema 'example' for set status" do + expect(test).to have_received(:assert_equal).with( + { 'title' => [ 'is required' ] }, + {} + ) + end end end end