Writing an API gem: how to write tests for an external API

Sozdanie gema 4

In the previous article we have found out how to get along with Hypermedia API and why at all we need it. Since the time I have written that article, I have managed to add all the API points to the gem code, and also made some minor bug fixes. Here is the link to all the changes since then: 8056904...c9088e6.

In this article we will fix the main weakness of the GrooveHQ gem: the lack of tests. Testing code that depends on an external API is an unpleasant but frequently encountered task. I will try to make it easier as much as I can.

If you are bothered by the question "why not TDD? What about green refactor?", then read the article about writing the first mkdev platform tests, there I explain why I don't like this approach.

Configuring the test environment

I choose Rspec as a library for tests again – I don't see any big advantages except the familiarity and greater popularity, but they are enough to make a choice. Let's update the Gemfile:

# ./Gemfile
# ...
group :test do
  gem 'rspec'
end

An attentive reader will ask: and why have I added the gem to the Gemfile but not to gemspec? The answer: having looked through popular gem sources and a couple of articles, I have made a conclusion that gems for tests are not a real gem dependency, hence you don't need to add them to gemspec.

Then you will need to add a folder spec and a file spec/spec_helper.rb:

# ./spec/spec_helper.rb
require_relative '../lib/groovehq # Loading the gem

RSpec.configure do |config|
end

Now we can make tests.

Code troubleshooting with tests

The first test I have written have caught a little bug in my code:

# ./spec/groovehq/resource_spec.rb
require 'spec_helper'

describe GrooveHQ::Resource do

  let(:client) { GrooveHQ::Client.new("phantogram") }

  it "returns empty data for invalid input" do
    resource = GrooveHQ::Resource.new(client, "")
    expect(resource.data).to eql({})
  end

end

It turns out, that if you pass an empty string as a parameter, the old version of code will throw an exception. The updated code doesn't have such a problem and lets the test to become green again:

# ./lib/groovehq/resource.rb
module GrooveHQ

  class Resource
    attr_reader :rels, :data

    def initialize(client, data)
      data = {} unless data.is_a?(Hash)

      @client = client
      @data = data.with_indifferent_access

      links = @data.delete(:links)
      links[:self] = @data.delete(:href) if @data.has_key?(:href)

      @rels = parse_links(links).with_indifferent_access
    end
# ...

Filled with enthusiasm by my first victory, I add several tests for the GrooveHQ::Resource:

# ./spec/groovehq/resource_spec.rb
# ...
  it "parses relations correctly" do
    data = {
      links: {
        assignee: {
          href: "https://api.groovehq.com/v1/agents/matt@groovehq.com"
        }
      }
    }
    resource = GrooveHQ::Resource.new(client, data)
    expect(resource.rels[:assignee]).to be_instance_of(GrooveHQ::Relation)
  end

  it "parses self relation correctly" do
    data = {
      href: "https://api.groovehq.com/v1/agents/matt@groovehq.com"
    }
    resource = GrooveHQ::Resource.new(client, data)
    expect(resource.rels[:self]).to be_instance_of(GrooveHQ::Relation)
  end

  it "parses data correctly" do
    data = {
      name: "When I am small"
    }
    resource = GrooveHQ::Resource.new(client, data)
    expect(resource.data[:name]).to eql "When I am small"
  end
# ...

I have also found one more bug, look the full commit with all the tests at this moment: f1b5da4. Having thought for a minute, I decided to split the tests for this class into contexts, where every context is devoted to a specific method: ed0cd7a.

Lyrical digression

Before I start to write tests, I want to talk about life. First of all, I have decided not to cover all methods, responsible for certain points, with tests. For example, this one:

      def customer(email)
        get("/customers/#{email}")
      end

The only test I could make up here is to make a stub of the HTTP-request result and compare if the call of this method returns the result of this stub. This seems like a very strange test. And we have 23 of these methods. There's no sense in stuffing the test folder with such useless checks which will be off the table after a minor API update.

So, all I need to do is write tests for the GrooveHQ::Client::Connection#parse_data method, and we can consider 80% of the important code covered with tests.

Testing requests to API

One conceptual problem arose right away: the #parse_data method is marked as a private, that means I can't call it directly, only with the help of send. And, all in all, one of the characteristics of private methods is that they don't need to be tested directly.

Unfortunately, that means we won't do without stubs, as all the public methods I can call make a request to the API. That means we need to "stab" these requests and, instead of executing the request, just return the ready JSON response. For this task I will use a gem WebMock, which I have linked in a commit 2bf8b92.

Here, instead of splitting by method contexts, I have made a context for the each type of response from the API. Here, for example, is the test for the easiest one - one root key with all the resource attributes inside:

# ./spec/groovehq/client/connection_spec.rb

# ...
  context "nested hash as response with single root key" do

    let(:response) do
      {
        ticket: {
          number: 1,
          summary: "This is the first 100 characters of the first message...",
          links: {
            assignee: {
              href: "https://api.groovehq.com/v1/agents/matt@groovehq.com"
            }
          }
        }
      }.to_json
    end

    before(:each) do
      stub_request(:get, "https://api.groovehq.com/v1/tickets/1").to_return(body: response)
    end

    it "returns resource for single root key" do
      expect(client.get("/tickets/1")).to be_instance_of(GrooveHQ::Resource)
    end

    it "returns resource with correct relations" do
      expect(client.get("/tickets/1").rels[:assignee].href).to eql("https://api.groovehq.com/v1/agents/matt@groovehq.com")
    end

  end
# ...

The line stub_request(:get, "https://api.groovehq.com/v1/tickets/1").to_return(body: response) means: "instead of executing the real request to this address, just return the response right away". The test turned out to be very heavy, that's why you need to go and look at the commit 69e56b4 for all the specs.

TravisCI

This, normally, could be the end of writing tests. But I have considered it a good idea to add Travis CI to the project. It is the most popular CI solution for OpenSource projects. It is pretty easy to add – just create a file .travis.yml and follow the documentation.

As a result, after every commit the tests will run and everybody who uses the gem could always check if it's properly functioning. For example, the GrooveHQ gem functions properly.

What's next?

At this moment, gem feature complete – you can link it to your application and use it for a comfortable inquiry to any GrooveHQ API points. We have covered with tests all the parts of the code we need, and the gem even has a beautiful icon from Travis, reporting that tests pass. Basically, we can stop here. But instead one more article is waiting for you, and a turn of work on improving gem – for me.

First of all, in the next article we will review metaprogramming in Ruby. I want to use something like that: client.ticket(2).state instead of client.ticket(2).rels[:state].get.data.

Secondly, we need to publish the gem somewhere, and also write the documentation for it.

So, a lot of great stuff ahead. If, in your opinion, there's something else that our gem needs – write in the comments and, probably, we will add some more articles ;-)


So at last you decided to start a self-education?

Then start with our free guide to the world of web development. You will find tons of useful advices and materials to learn inside the book.

Get the book Cover huge en