├── .gitignore ├── Gemfile ├── Gemfile.lock ├── README.md ├── features ├── cup_size_list.feature ├── fruit_list.feature ├── step_definitions │ ├── common_steps.rb │ ├── cup_size_list_steps.rb │ └── fruit_list_steps.rb └── support │ ├── core_ext.rb │ ├── env.rb │ ├── transforms.rb │ └── validations.rb └── fruit_app.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | gem "multi_json" 3 | gem "sinatra" 4 | 5 | group :test do 6 | gem "cucumber" 7 | gem "rspec" 8 | gem "rack-test" 9 | gem "activesupport" 10 | end -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | activesupport (4.0.2) 5 | i18n (~> 0.6, >= 0.6.4) 6 | minitest (~> 4.2) 7 | multi_json (~> 1.3) 8 | thread_safe (~> 0.1) 9 | tzinfo (~> 0.3.37) 10 | atomic (1.1.14) 11 | builder (3.2.2) 12 | cucumber (1.3.10) 13 | builder (>= 2.1.2) 14 | diff-lcs (>= 1.1.3) 15 | gherkin (~> 2.12) 16 | multi_json (>= 1.7.5, < 2.0) 17 | multi_test (>= 0.0.2) 18 | diff-lcs (1.2.5) 19 | gherkin (2.12.2) 20 | multi_json (~> 1.3) 21 | i18n (0.6.9) 22 | minitest (4.7.5) 23 | multi_json (1.8.4) 24 | multi_test (0.0.3) 25 | rack (1.5.2) 26 | rack-protection (1.5.2) 27 | rack 28 | rack-test (0.6.2) 29 | rack (>= 1.0) 30 | rspec (2.14.1) 31 | rspec-core (~> 2.14.0) 32 | rspec-expectations (~> 2.14.0) 33 | rspec-mocks (~> 2.14.0) 34 | rspec-core (2.14.7) 35 | rspec-expectations (2.14.4) 36 | diff-lcs (>= 1.1.3, < 2.0) 37 | rspec-mocks (2.14.4) 38 | sinatra (1.4.4) 39 | rack (~> 1.4) 40 | rack-protection (~> 1.4) 41 | tilt (~> 1.3, >= 1.3.4) 42 | thread_safe (0.1.3) 43 | atomic 44 | tilt (1.4.1) 45 | tzinfo (0.3.38) 46 | 47 | PLATFORMS 48 | ruby 49 | 50 | DEPENDENCIES 51 | activesupport 52 | cucumber 53 | multi_json 54 | rack-test 55 | rspec 56 | sinatra 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # REST testing with Cucumber 2 | 3 | This repo contains the example code for my blog post about [effective API testing with cucumber](http://localhost:3000/blog/effective-api-testing-with-cucumber). Please read it for details. 4 | 5 | To run the features it's as simple as: 6 | 7 | $ bundle 8 | $ cucumber -------------------------------------------------------------------------------- /features/cup_size_list.feature: -------------------------------------------------------------------------------- 1 | Feature: Cup size list 2 | In order to make a great smoothie 3 | I need a cup of the right size 4 | 5 | Scenario: List cup sizes 6 | Given the system knows about the following cup sizes: 7 | | name | fluid ounces | 8 | | Regular | 12 | 9 | | Large | 16 | 10 | When the client requests a list of cup sizes 11 | Then the response is a list containing two cup sizes 12 | And one cup sizes has the following attributes: 13 | | attribute | type | value | 14 | | name | String | Regular | 15 | | fluid ounces | Integer | 12 | 16 | And one cup sizes has the following attributes: 17 | | attribute | type | value | 18 | | name | String | Large | 19 | | fluid ounces | Integer | 16 | -------------------------------------------------------------------------------- /features/fruit_list.feature: -------------------------------------------------------------------------------- 1 | Feature: Fruit list 2 | In order to make a great smoothie 3 | I need some fruit 4 | 5 | Scenario: List fruit (original) 6 | Given the system knows about the following fruit: 7 | | name | color | 8 | | banana | yellow | 9 | | strawberry | red | 10 | When the client requests GET /fruits 11 | Then the response should be JSON: 12 | """ 13 | [ 14 | {"name": "banana", "color": "yellow"}, 15 | {"name": "strawberry", "color": "red"} 16 | ] 17 | """ 18 | 19 | Scenario: List fruit (modified) 20 | Given the system knows about the following fruit: 21 | | name | color | 22 | | banana | yellow | 23 | | strawberry | red | 24 | When the client requests a list of fruit 25 | Then the response is a list containing two fruits 26 | And one fruit has the following attributes: 27 | | attribute | type | value | 28 | | name | String | banana | 29 | | color | String | yellow | 30 | And one fruit has the following attributes: 31 | | attribute | type | value | 32 | | name | String | strawberry | 33 | | color | String | red | -------------------------------------------------------------------------------- /features/step_definitions/common_steps.rb: -------------------------------------------------------------------------------- 1 | require "active_support/inflector" 2 | 3 | When(/^the client requests GET (.*)$/) do |path| 4 | get(path) 5 | end 6 | 7 | When(/^the client requests a list of (.*?)s?$/) do |type| 8 | get("/#{type.pluralize.downcase.tr(' ', '-')}") 9 | end 10 | 11 | Then(/^the response should be JSON:$/) do |json| 12 | expect(MultiJson.load(last_response.body)).to eq(MultiJson.load(json)) 13 | end 14 | 15 | Then(/^the response is a list containing (#{CAPTURE_INT}) (.*?)s?$/) do |count, type| 16 | data = MultiJson.load(last_response.body) 17 | validate_list(data, of: type, count: count) 18 | end 19 | 20 | Then(/(#{CAPTURE_INT}) (?:.*?) ha(?:s|ve) the following attributes:$/) do |count, table| 21 | expected_item = table.hashes.each_with_object({}) do |row, hash| 22 | name, value, type = row["attribute"], row["value"], row["type"] 23 | hash[name.tr(" ", "_").camelize(:lower)] = value.to_type(type.constantize) 24 | end 25 | data = MultiJson.load(last_response.body) 26 | matched_items = data.select { |item| item == expected_item } 27 | expect(matched_items.count).to eq(count) 28 | end -------------------------------------------------------------------------------- /features/step_definitions/cup_size_list_steps.rb: -------------------------------------------------------------------------------- 1 | Given(/^the system knows about the following cup sizes:$/) do |cup_sizes| 2 | FruitApp.cup_sizes = cup_sizes.hashes.map do |hash| 3 | hash.each_with_object({}) do |(k, v), h| 4 | h[k.tr(" ", "_").camelize(:lower)] = v =~ /\d+/ ? v.to_i : v 5 | end 6 | end 7 | end -------------------------------------------------------------------------------- /features/step_definitions/fruit_list_steps.rb: -------------------------------------------------------------------------------- 1 | Given(/^the system knows about the following fruit:$/) do |fruits| 2 | FruitApp.data = fruits.hashes 3 | end -------------------------------------------------------------------------------- /features/support/core_ext.rb: -------------------------------------------------------------------------------- 1 | module Boolean; end 2 | class TrueClass; include Boolean; end 3 | class FalseClass; include Boolean; end 4 | 5 | module Enum; end 6 | class String; include Enum; end 7 | 8 | class String 9 | def to_type(type) 10 | # cannot use 'case type' which checks for instances of a type rather than type equality 11 | if type == Boolean then self =~ /true/i 12 | elsif type == Date then Date.parse(self) 13 | elsif type == DateTime then DateTime.parse(self) 14 | elsif type == Enum then self.upcase.tr(" ", "_") 15 | elsif type == Float then self.to_f 16 | elsif type == Integer then self.to_i 17 | else self 18 | end 19 | end 20 | end -------------------------------------------------------------------------------- /features/support/env.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), "..", "..", "fruit_app") 2 | require "rack/test" 3 | 4 | module KnowsAboutTheFruitApp 5 | def app 6 | FruitApp.new 7 | end 8 | end 9 | 10 | World(KnowsAboutTheFruitApp, Rack::Test::Methods) -------------------------------------------------------------------------------- /features/support/transforms.rb: -------------------------------------------------------------------------------- 1 | CAPTURE_INT = Transform(/^(?:-?\d+|zero|one|two|three|four|five|six|seven|eight|nine|ten)$/) do |v| 2 | %w(zero one two three four five six seven eight nine ten).index(v) || v.to_i 3 | end -------------------------------------------------------------------------------- /features/support/validations.rb: -------------------------------------------------------------------------------- 1 | def validate_list(data, of: nil, count: nil) 2 | expect(data).to be_a_kind_of(Array) 3 | expect(data.count).to eq(count) unless count.nil? 4 | unless of.nil? 5 | validate_item = "validate_#{of.singularize.downcase.tr(' ', '_')}".to_sym 6 | data.each { |item| send(validate_item, item) } 7 | end 8 | end 9 | 10 | def validate_fruit(data) 11 | expect(data["name"]).to be_a_kind_of(String) 12 | expect(data["name"]).to_not be_empty 13 | expect(data["color"]).to be_a_kind_of(String) 14 | expect(data["color"]).to match(/^(green|purple|red|yellow)$/i) 15 | end 16 | 17 | def validate_cup_size(data) 18 | expect(data["name"]).to be_a_kind_of(String) 19 | expect(data["name"]).to_not be_empty 20 | expect(data["fluidOunces"]).to be_a_kind_of(Integer) 21 | expect(data["fluidOunces"]).to be >= 0 22 | end -------------------------------------------------------------------------------- /fruit_app.rb: -------------------------------------------------------------------------------- 1 | require "sinatra" 2 | 3 | class FruitApp < Sinatra::Base 4 | set :data, "" 5 | set :cup_sizes, "" 6 | 7 | get "/fruits", provides: :json do 8 | MultiJson.dump(settings.data) 9 | end 10 | 11 | get "/cup-sizes", provides: :json do 12 | MultiJson.dump(settings.cup_sizes) 13 | end 14 | end --------------------------------------------------------------------------------