Skip to content

Instantly share code, notes, and snippets.

@gonzalo-bulnes
Forked from aeden/api_steps.rb
Last active February 3, 2021 16:49
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save gonzalo-bulnes/7069339 to your computer and use it in GitHub Desktop.
Save gonzalo-bulnes/7069339 to your computer and use it in GitHub Desktop.
A set of Cucumber steps to test and document API behaviour (with verbose and DRY step definitions for an example resource).Deeply inspired in @aeden API steps, see http://vimeo.com/30586709
# features/step_definitions/api_steps.rb
# These steps are very deeply inspired in the Anthony Eden (@aeden) API steps.
# See http://vimeo.com/30586709
# Given
Given /^I send and accept JSON$/ do
header 'Accept', 'application/json'
header 'Content-Type', 'application/json'
end
Given /^I send and accept JSON using version (\d+) of the (\w+) API$/ do |version, model|
header 'Accept', "application/vnd.shopping-list+json; version=#{version}"
header 'Content-Type', "application/json"
end
Given /^I send and accept XML$/ do
header 'Accept', 'text/xml'
header 'Content-Type', 'text/xml'
end
# When
When /^I send a (\w+) request to "(.*?)"$/ do |method, path|
send(method.downcase, path)
end
When /^I send a POST request to "([^\"]*)" with the following:$/ do |path, body|
post path, body
end
# Then
Then /^the response status should be "([^"]*)"$/ do |status|
begin
last_response.status.should eq(status.to_i)
rescue RSpec::Expectations::ExpectationNotMetError => e
puts "Response body:"
puts last_response.body
raise e
end
end
Then /^the response body should be a JSON representation of the (\w+)$/ do |model|
last_response.body.should eq(model.constantize.last.to_json)
end
Then /^the response body should be a JSON representation of the (\w+) list$/ do |model|
last_response.body.should eq(model.constantize.all.to_json)
end
Then /^the response body should be a XML representation of the (\w+)$/ do |model|
last_response.body.should eq(model.constantize.last.to_xml)
end
Then /^the response body should be a XML representation of the (\w+) list$/ do |model|
last_response.body.should eq(model.constantize.all.to_xml)
end
Then /^the response body should have (\d+) (?:\w)$/ do |count|
JSON.parse(last_response.body).length.should eq(count.to_i)
end
Then /^the XML response body should have (\d+) (.*)$/ do |count, model|
Nokogiri::XML(last_response.body).search("/#{model.downcase.tr(" ", "-")}/#{model.downcase.tr(" ", "-").singularize}").size.should eq(count.to_i)
end
Then /^show me the (unparsed)?\s?response$/ do |unparsed|
if unparsed == 'unparsed'
puts last_response.body
elsif last_response.headers['Content-Type'] =~ /json/
json_response = JSON.parse(last_response.body)
puts JSON.pretty_generate(json_response)
elsif last_response.headers['Content-Type'] =~ /xml/
puts Nokogiri::XML(last_response.body)
else
puts last_response.headers
puts last_response.body
end
end
Then /^the (\w+) in the JSON representation of the (\w+) should be "([^"]*)"$/ do |attribute_name, model, attribute_value|
json = JSON.parse(last_response.body)
match = JSONSelect(".#{attribute_name}").match(json)
match.should eq(attribute_value)
end
Then /^the response body should be a JSON v(\d+) representation of the (\w+)$/ do |version, model|
last_response.body.should eq(model.capitalize.constantize.last.send("to_json_v#{version}"))
end
# features/step_definitions/authentication_steps.rb
Given(/^I have a valid authentication token$/) do
@current_user ||= User.find_by(email: "me@example.com")
@current_user ||= FactoryGirl.create(:me)
end
# features/products/create_product.feature
Feature: create a product
As a ...
In order to ...
I want to create a product
Scenario: create a product with JSON
Given I send and accept JSON
And I have a valid authentication token
When I send a POST request to "/products?user_email=me@example.com&user_token=ExaMpLeTokEn" with the following:
"""
{
"product": {
"name":"Carrots"
}
}
"""
Then the response status should be "200"
And the response body should be a JSON representation of the created Product (Ember Data conventions)
And show me the response
Scenario: create a product as somebody else with JSON
Given I send and accept JSON
And I have a valid authentication token
When I send a POST request to "/products?user_email=me@example.com&user_token=ExaMpLeTokEn" with the following:
"""
{
"product": {
"name":"Carrots",
"owner_id": 1
}
}
"""
Then the response status should be "200"
And the response body should be a JSON representation of the created Product (Ember Data conventions)
And show me the response
Scenario: create a product with XML
Given I send and accept XML
And I have a valid authentication token
When I send a POST request to "/products?user_email=me@example.com&user_token=ExaMpLeTokEn" with the following:
"""
<?xml version="1.0"?>
<product>
<name>Carrots</name>
</product>
"""
Then the response status should be "201"
And the response body should be a XML representation of the Product
And show me the response
# features/products/destroy_product.feature
Feature: destroy a product
As an API client
In order to remove a buying option
I can destroy a product
Scenario: destroy a product with JSON
Given I send and accept JSON
And I have a valid authentication token
And I have created 1 Product
When I send a DELETE request to "/products/{{product_id}}?user_email=me@example.com&user_token=ExaMpLeTokEn" (Product)
Then the response status should be "204"
Scenario: destroy a product owned by somebody else with JSON
Given I send and accept JSON
And I have a valid authentication token
And somebody else has created 1 Product
When I send a DELETE request to "/products/{{product_id}}?user_email=me@example.com&user_token=ExaMpLeTokEn" (Product)
Then the response status should be "404"
Scenario: destroy a product with XML
Given I send and accept XML
And I have a valid authentication token
And I have created 1 Product
When I send a DELETE request to "/products/{{product_id}}?user_email=me@example.com&user_token=ExaMpLeTokEn" (Product)
Then the response status should be "204"
# features/step_definitions/ember_data_compliance_steps.rb
Then /^the response body should fulfill the Ember Data expectations for (\w+)( list)?$/ do |model, multiple|
json = JSON.parse(last_response.body)
# The JSON response should be wrapped with it's model name,
# see http://emberjs.com/guides/models/the-rest-adapter/#toc_json-root
# Example:
# {
# "product": {
# "id": 1,
# "name": "carrots"
# }
# }
#
JSONSelect(".#{multiple ? model.underscore.pluralize : model.underscore}").test(json).should be_true
end
# features/products/list_products.feature
Feature: list products
As an API client
In order to get all of items I can buy
I can list products
Scenario: list products with JSON
Given I send and accept JSON
And I have a valid authentication token
And I have created 2 Products
When I send a GET request to "/products?user_email=me@example.com&user_token=ExaMpLeTokEn"
Then the response status should be "200"
And the response body should be a JSON representation of the Products list (Ember Data conventions)
And the JSON response body should list all Products (Ember Data conventions)
And show me the response
Scenario: list products created by others with JSON
Given I send and accept JSON
And I have a valid authentication token
And I have created 3 Products
And somebody else has created 2 Products
When I send a GET request to "/products?user_email=me@example.com&user_token=ExaMpLeTokEn"
Then the response status should be "200"
And the JSON response body should list 3 Products (Ember Data conventions)
And show me the response
Scenario: list products with XML
Given I send and accept XML
And I have a valid authentication token
And I have created 2 Products
When I send a GET request to "/products?user_email=me@example.com&user_token=ExaMpLeTokEn"
Then the response status should be "200"
And the response body should be a XML representation of the Product list
And the XML response body should have 2 products
And show me the response
# spec/factories/products.rb
FactoryGirl.define do
factory :product do
name "Apples"
owner
end
end
# If you want to DRY your step definitions and forget about resources_steps.rb file,
# you can include (once and for all!) scaffolded_resources_steps.rb in your step definitions.
# features/step_definitions/products_steps.rb
# Given
Given /^I have created (\d+) Product(?:s)?$/ do |n|
@products ||= []
n.to_i.times do
@products << FactoryGirl.create(:product, owner_id: @current_user.id)
end
end
Given /^somebody else has created (\d+) Product(?:s)?$/ do |n|
# @user doesn't need to be stable and can be generated often.
# On the contrary, @current_user must be stable.
# see `Given I have a valid authentication token`
@user ||= FactoryGirl.create(:user)
@products ||= []
n.to_i.times do
@products << FactoryGirl.create(:product, owner_id: @user.id)
end
end
# When
When /^I send a (\w+) request to "(.*?)" \(Product\)$/ do |method, path|
path = Mustache.render(path, {product_id: @products.last.id})
send(method.downcase, path)
end
When /^I send a (\w+) request to "(.*?)" with the following \(Product\):$/ do |method, path, body|
path = Mustache.render(path, {product_id: @products.last.id})
send(method.downcase, path, body)
end
# Then
Then /^the response body should be a JSON representation of the( created)? Product \(Ember Data conventions\)$/ do |product_was_created|
steps %{
Then the response body should fulfill the Ember Data expectations for Product
}
if product_was_created
puts "Internal index was updated after Product creation."
@products ||= []
@products << Product.last
end
json = JSON.parse(last_response.body)
match = JSONSelect(".product").match(json)
ActiveSupport::JSON.encode(match).should eq(@products.last.to_json)
end
Then /^the response body should be a JSON representation of the Products list \(Ember Data conventions\)$/ do
steps %{
Then the response body should fulfill the Ember Data expectations for Product list
}
json = JSON.parse(last_response.body)
match = JSONSelect(".products").match(json)
ActiveSupport::JSON.encode(match).should eq(@products.to_json)
end
Then /^the JSON response body should list all Products \(Ember Data conventions\)$/ do
steps %{
Then the response body should fulfill the Ember Data expectations for Product list
}
json = JSON.parse(last_response.body)
JSONSelect(".products").match(json).length.should eq(@products.count)
end
Then /^the JSON response body should list (\d+) Products \(Ember Data conventions\)$/ do |n_products|
steps %{
Then the response body should fulfill the Ember Data expectations for Product list
}
json = JSON.parse(last_response.body)
JSONSelect(".products").match(json).length.should eq(n_products.to_i)
end
# features/step_definitions/scaffolded_resources_steps.rb
# This file replaces all features/step_definitions/<resources>_steps.rb at once.
# Steps are a bit less easy to read, that's why I commented them a lot.
#
# It's up to you to keep creating the scaffold steps for each resource, however
# once I get more than six or seven models in an application, I personally prefer this version.
# Given
# Params:
# n - number of instances to create
# model - the resource model name
#
# Example:
# Given I have created 5 Products
# Given I have created 1 ShoppingList
#
Given /^I have created (\d+) (\w+)$/ do |n, model|
# Caution: model can be either a singluar or a pluralized model name.
# Set an instance variable to reference the Model instances
# @models
#
# See http://apidock.com/ruby/Object/instance_variable_set
instance_variable_name = ('@' + model.underscore.pluralize).to_sym
instance_variable_value = instance_variable_get(instance_variable_name) || []
instance_variable_set(instance_variable_name, instance_variable_value)
# Create instances of Model
n.to_i.times do
# Remember model can be either a singluar or a pluralized model name.
factory_name = model.underscore.singularize.to_sym # :model
instance_variable_get(instance_variable_name) << FactoryGirl.create(factory_name)
end
end
# When
# Params:
# method - HTTP verb
# path - URI with Mustache inclusion for ID (see conventions)
# model - the resource model name
#
# Conventions:
# - The URI Mustache segments are ID placeholders.
# - Each segment follws the pattern: {{model_id}}
# - The model name corresponding to each segment is explicitely listed: (Model)
#
# Example:
# When I send a GET request to "/users/{{user_id}}/books" (User)
#
When /^I send a (\w+) request to "(.*?)" \((\w+)\)$/ do |method, path, model|
# Set an instance variable to reference the Model instances
# @models
#
# See http://apidock.com/ruby/Object/instance_variable_set
instance_variable_name = ('@' + model.underscore.pluralize).to_sym
instance_variable_value = instance_variable_get(instance_variable_name) || []
instance_variable_set(instance_variable_name, instance_variable_value)
# URL placeholder for the Model instance ID
# :model_id
instance_id_symbol = (model.underscore.singularize + '_id').to_sym
path = Mustache.render(path, {instance_id_symbol => instance_variable_get(instance_variable_name).last.id})
send(method.downcase, path)
end
# Params:
# method - HTTP verb
# path - URI with Mustache inclusion for ID (see conventions)
# model - the resource model name
# body - the request body
#
# Conventions:
# - The URI Mustache segments are ID placeholders.
# - Each segment follws the pattern: {{model_id}}
# - The model name corresponding to each segment is explicitely listed: (Model)
# - The request body is written with triple quotes.
#
# Example:
# When I send a POST request to "/users/{{user_id}}/books" with the following (User):
# """
# {
# "book": {
# "title": "The RSpec Book",
# "author": "Chelimsky, David"
# }
# }
# """
#
When /^I send a (\w+) request to "(.*?)" with the following \((\w+)\):$/ do |method, path, model, body|
# Set an instance variable to reference the Model instances
# @models
#
# See http://apidock.com/ruby/Object/instance_variable_set
instance_variable_name = ('@' + model.underscore.pluralize).to_sym
instance_variable_value = instance_variable_get(instance_variable_name) || []
instance_variable_set(instance_variable_name, instance_variable_value)
# URL placeholder for the Model instance ID
# :model_id
instance_id_symbol = (model.underscore.singularize + '_id').to_sym
path = Mustache.render(path, {instance_id_symbol => instance_variable_get(instance_variable_name).last.id})
send(method.downcase, path, body)
end
# Then
# Params:
# instance_was_created_flag - when not blank triggers the instances index update (use it for creation sceanrii)
# model - the resource model name
#
# Example:
# Then the JSON response body should be a JSON representation of the Product (Ember Data conventions)
# Then the JSON response body should be a JSON representation of the created Product (Ember Data conventions)
#
Then /^the response body should be a JSON representation of the( created)? (\w+) \(Ember Data conventions\)$/ do |instance_was_created_flag, model|
# Caution: model shouldn't but could be either a singluar or a pluralized model name.
steps %Q{
Then the response body should fulfill the Ember Data expectations for #{model.camelize.singularize}
}
# Set an instance variable to reference the Model instances
# @models
#
# See http://apidock.com/ruby/Object/instance_variable_set
instance_variable_name = ('@' + model.underscore.pluralize).to_sym
instance_variable_value = instance_variable_get(instance_variable_name) || []
instance_variable_set(instance_variable_name, instance_variable_value)
if instance_was_created_flag.presence
# Reference the last created instance
instance_variable_get(instance_variable_name) << model.camelize.singularize.constantize.last
Kernel.puts "Internal index was updated after #{model.camelize.singularize} creation."
end
# If you don't want to folllow the Ember Data conventions, adapt this to yours.
json = JSON.parse(last_response.body)
match = JSONSelect(".#{model.underscore.singularize}").match(json)
ActiveSupport::JSON.encode(match).should eq(instance_variable_get(instance_variable_name).last.to_json)
end
# Params:
# model - the resource model name
#
# Example:
# Then the response body should be a JSON representation of the Shoes list (Ember Data conventions)
# Then the response body should be a JSON representation of the Shoe list (Ember Data conventions)
#
Then /^the response body should be a JSON representation of the (\w+) list \(Ember Data conventions\)$/ do |model|
# Caution: model shouldn't but could be either a singluar or a pluralized model name.
steps %{
Then the response body should fulfill the Ember Data expectations for #{model.camelize.singularize} list
}
# An instance variable that references the Model instances
# should have been set in a previous 'Given' step.
# @models
# Wee acced it with: instance_variable_get(instance_variable_name)
instance_variable_name = ('@' + model.underscore.pluralize).to_sym
json = JSON.parse(last_response.body)
match = JSONSelect(".#{model.underscore.pluralize}").match(json)
ActiveSupport::JSON.encode(match).should eq(instance_variable_get(instance_variable_name).to_json)
end
# Params:
# model - the resource model name
#
# Example:
# Then the JSON response body should list all Products (Ember Data conventions)
# Then the JSON response body should list all Product (Ember Data conventions)
#
Then /^the JSON response body should list all (\w+) \(Ember Data conventions\)$/ do |model|
# Caution: model shouldn't but could be either a singluar or a pluralized model name.
steps %{
Then the response body should fulfill the Ember Data expectations for #{model.camelize.singularize} list
}
# An instance variable that references the Model instances
# should have been set in a previous 'Given' step.
# @models
# We acceed it with: instance_variable_get(instance_variable_name)
instance_variable_name = ('@' + model.underscore.pluralize).to_sym
json = JSON.parse(last_response.body)
match = JSONSelect(".#{model.underscore.pluralize}").match(json)
match.length.should eq(instance_variable_get(instance_variable_name).count)
end
# Params:
# model - the resource model name
#
# Example:
# Then the JSON response body should list 5 Products (Ember Data conventions)
# Then the JSON response body should list 1 Product (Ember Data conventions)
#
Then /^the JSON response body should list (\d+) (\w+) \(Ember Data conventions\)$/ do |n, model|
# Caution: model can be either a singluar or a pluralized model name.
steps %{
Then the response body should fulfill the Ember Data expectations for #{model.camelize.singularize} list
}
json = JSON.parse(last_response.body)
match = JSONSelect(".#{model.underscore.pluralize}").match(json)
match.length.should eq(n.to_i)
end
# features/products/show_product.feature
Feature: show a product
As an API client
In order to see a single buying option
I can show a product
Scenario: show a product with JSON
Given I send and accept JSON
And I have a valid authentication token
And I have created 1 Product
When I send a GET request to "/products/{{product_id}}?user_email=me@example.com&user_token=ExaMpLeTokEn" (Product)
Then the response status should be "200"
And the response body should be a JSON representation of the Product (Ember Data conventions)
And show me the response
Scenario: show a product owned by somebody else with JSON
Given I send and accept JSON
And I have a valid authentication token
And somebody else has created 1 Product
When I send a GET request to "/products/{{product_id}}?user_email=me@example.com&user_token=ExaMpLeTokEn" (Product)
Then the response status should be "404"
And show me the response
Scenario: show a product with XML
Given I send and accept XML
And I have a valid authentication token
And I have created 1 Product
When I send a GET request to "/products/{{product_id}}?user_email=me@example.com&user_token=ExaMpLeTokEn" (Product)
Then the response status should be "200"
And the response body should be a XML representation of the Product
And show me the response
# features/products/update_product.feature
Feature: update a product
As an API client
In order to update a buying option
I can update a product
Scenario: update a product with JSON
Given I send and accept JSON
And I have a valid authentication token
And I have created 1 Product
When I send a PUT request to "/products/{{product_id}}?user_email=me@example.com&user_token=ExaMpLeTokEn" with the following (Product):
"""
{
"product": {
"name": "Salad"
}
}
"""
Then the response status should be "204"
And show me the response
Scenario: update a product owned by somebody else with JSON
Given I send and accept JSON
And I have a valid authentication token
And somebody else has created 1 Product
When I send a PUT request to "/products/{{product_id}}?user_email=me@example.com&user_token=ExaMpLeTokEn" with the following (Product):
"""
{
"product": {
"name": "Salad"
}
}
"""
Then the response status should be "404"
And show me the response
Scenario: update a product with XML
Given I send and accept XML
And I have a valid authentication token
And I have created 1 Product
When I send a PUT request to "/products/{{product_id}}?user_email=me@example.com&user_token=ExaMpLeTokEn" with the following (Product):
"""
<?xml version="1.0"?>
<product>
<name>Carrots</name>
</product>
"""
Then the response status should be "204"
# spec/factories/users.rb
FactoryGirl.define do
sequence :email do |n|
"user#{n}@factory.com"
end
factory :user do
email
name "test_user"
provider "open_id"
uid 1
end
factory :owner, parent: :user
factory :me, parent: :user do
email "me@example.com"
authentication_token "ExaMpLeTokEn"
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment