Snapi

Section 10

Testing

Testing

  1. App/Routing

  2. Curator

  3. Mapper

  4. Serializer

App/Routing

This lives in the x_endpoint_spec.rb file.

RSpec.describe "Contacts Endpoint", :type => :feature do
  describe "fetching the root collection" do
    it "returns base collection response", :skip_auth do
      get "/contacts"
      expect(collection["href"]).to eq("http://example.org/v3/contacts")
    end
  end

  describe "fetching specific objects" do
  end

  describe "creating objects" do
  end

  describe "updating objects" do
  end

  describe "destroying objects" do
  end

  describe "searching for objects" do
  end
end

App/Routing

This lives in the x_endpoint_spec.rb file.

RSpec.describe "Contacts Endpoint", :type => :feature do
  describe "fetching specific objects" do
    it "returns 404 on missing" do
      get "/contacts/1"
      expect(last_response).to be_not_found
    end

    it "returns single requested object" do
      team = create_team
      member = create_auth_member(:team_id => team.id)
      contact = create_contact(:member_id => member.id)
      get "/contacts/#{contact.id}"
      expect(items[0].id).to eq(contact.id)
    end
  end
end

helpers

rspec/support/object_creation_helpers.rb

This file defines all the helpers that you will be using to write specs.

i.e. create_team, create_contact, create_auth_member 

If a specific function doesn't exist it will use meta programming to figure out the ruby object to create. i.e. create_sms_gateway

helpers

def create_team(attributes = {})
  plan = attributes.delete(:plan) { FREE_PLAN }
  defaults = {:sport_id => 1}
  super(defaults.merge(attributes))
    .tap { |t| assign_plan(t, :plan_id => plan.id) }
end

Collection+JSON

Collection+JSON is a JSON-based read/write hypermedia-type designed to support management and querying of simple collections.

 

It uses a JSON structure to contains things like collections, refs, templates, queries, and commands.

https://github.com/collection-json/spec

include magic

class ContactApp < BaseApp
  include Omicron.curation(ContactCurator)
  include Omicron.serialization(ContactSerializer)

  get "/contacts" do
    pipe(default_response, :through => [
      :serialize
    ])
  end

  get "/contacts/search" do
    response = curate(artifact, :action => :search)
    serialize(response)
  end
end

We use the include magic to
inject serialize into the app. 

class Response
  include Virtus.value_object

  values do
    attribute :status, Integer, :default => 200
    attribute :headers, Hash, :default => {}
    attribute :collection, Array, :default => []
    attribute :original_collection, Array, :default => []
    attribute :page_information, Hash
    attribute :error_message, String
  end
end

include magic

Omicron.serialization takes the response and will return one of three things:

  1. Errors if response.error_message
  2. "" if no response.no_content?
  3. Call serialize on Serializer and return JSON
class ContactApp < BaseApp
  include Omicron.serialization(ContactSerializer)

  get "/contacts/search" do
    response = curate(artifact, :action => :search)
    serialize(response)
  end
end

Scribe

All serializers inherit from Scribe which deals with the details of serialization. This allows you to only need a few functions in your serializer.

  1. search_url
  2. base_collection
  3. as_item

Serializer

class ContactSerializer < Scribe
  private

  def search_url(*args)
    search_contacts_url(*args)
  end

  def base_collection
    ...
  end

  def as_item(object)
    ...
  end
end

search_url

class ContactSerializer < Scribe
  private

  def search_url(*args)
    search_contacts_url(*args)
  end
end

base_collection

Returned for every request. Contains:

  • version  -- the current version of the api
  • href     -- href of this collection
  • rel      -- unique identifier
  • template -- for creating/updating
  • links    -- associated links
  • queries  -- list of queries
  • commands -- list of commands
class ContactSerializer < Scribe
  private

  def base_collection
    {
      collection: {
        version: API_VERSION,
        href: contacts_url,
        rel: "contacts",
        template: {
          data: [
            {name: "label", value: nil},
            {name: "type", value: Contact.type}
          ]
        },
        links: [
          {rel: "contact_email_addresses", href: contact_email_addresses_url},
          {rel: "self", href: request_url}
        ],
        queries: [
          {
            rel: "search",
            href: search_contacts_url,
            data: [
              {name: "team_id", value: nil},
              {name: "can_receive_push_notifications", value: nil}
            ]
          }
        ],
        commands: [
          {
            rel: "create_bulk_contacts",
            href: create_bulk_contacts_contacts_url,
            data: [
              {name: "team_id", value: nil},]
            ]
          }
        ]
      }
    }
  end
end

as_item

Called for every item in response.collection.

  • href  -- href of this collection
    
  • data  -- data for the item
  • links -- associated links
class ContactSerializer < Scribe
  private

  def as_item(object)
    {
      href: contact_url(object.id),
      data: [
        {name: "id", value: object.id},
        {name: "type", value: object.type},
        {name: "address_city", value: object.address_city},
        {name: "address_country", value: object.address_country},
        {name: "address_state", value: object.address_state},
        {name: "address_street1", value: object.address_street1},
        {name: "address_street2", value: object.address_street2},
        {name: "address_zip", value: object.address_zip}
        {name: "created_at", value: object.created_at, type: "DateTime"},
        {name: "updated_at", value: object.updated_at, type: "DateTime"},
      ],
      links: [
        {
          rel: "contact_email_addresses", 
          href: search_contact_email_addresses_url(:contact_id => object.id)
        },
        {
          rel: "contact_phone_numbers", 
          href: search_contact_phone_numbers_url(:contact_id => object.id)
        },
        {rel: "member", href: member_url(object.member_id)},
      ]
    }
  end
end
{
  "collection": {
    "version": "3.454.0",
    "href": "http://localhost:3000/contacts",
    "rel": "contacts",
    "template": {
      "data": [
        {
          "name": "label",
          "value": null
        },
        {
          "name": "address_street1",
          "value": null
        },
        ...
        {
          "name": "type",
          "value": "contact"
        }
      ]
    },
    "links": [
      {
        "rel": "contact_email_addresses",
        "href": "http://localhost:3000/contact_email_addresses"
      },
      ....
      {
        "rel": "self",
        "href": "http://localhost:3000/contacts"
      }
    ],
    "queries": [
      {
        "rel": "search",
        "href": "http://localhost:3000/contacts/search",
        "data": [
          {
            "name": "team_id",
            "value": null
          },
        ]
      }
    ]
  }
}
/contacts
{
  "collection": {
    "version": "3.454.0",
    "href": "http://localhost:3000/contacts",
    "rel": "contacts",
    "template": {
      ...
    },
    "links": [
      ....
    ],
    "queries": [
      ...
    ],
    "items": [
      {
        "href": "http://localhost:3000/contacts/1",
        "data": [
          {
            "name": "id",
            "value": 1
          },
          {
            "name": "type",
            "value": "contact"
          },
          {
            "name": "member_id",
            "value": 63
          },
          {
            "name": "created_at",
            "value": "2017-05-10T17:28:05Z",
            "type": "DateTime"
          },
        ],
        "links": [
          {
            "rel": "contact_email_addresses",
            "href": "http://localhost:3000/contact_email_addresses/search?contact_id=1"
          },
        ],
        "rel": "contact-1"
      }
    ]
  }
}
/contacts/1

Serializer

Responsibilities

  1. Converts response into Collection JSON

https://github.com/collection-json/spec

Thank you!

Snapi Section 10 Testing

By Dustin McCraw

Snapi Section 10 Testing

  • 842