Stubbing Web Services with Sinatra: Standing up a quick server for your client application

Posted on Jul 15, 2014

Introduction

The concept of dependencies seems rather straight forward. If “Thing A” depends on “Thing B”, then we can understand that we can't possibly use “Thing A” until we have “Thing B”. It's not even a development principle really, it's more of an “even small kids realize this” kind of principle. It's sort of a universal given, unless you happen to be Gallifreyan.

Taking that a step further, if you want me to make an application for you using some web services you've published, it might be prudent to give me access to those services, right? “Yeah, we haven't actually written the services yet, but we've figured out the API”. Oh. Awkward. We can mitigate some of the problem with unit tests, but how do we test actual hands on usage? There are a few possibilities here, but one that I'm rather partial to is mocking up some services. Even provided access, there are some benefits to making a mock of an external service, such as easier testing of fault tolerance, and avoiding unnecessary calls to paid services.

Getting Started

While we could throw together some services in almost any language with any number of frameworks, as a matter of convenience, I'm going to go with Sinatra on Ruby. I'm assuming that Ruby is already installed on the local system.

To start, we need to create a Gemfile to list our dependencies. For this project we'll end up using two gems.

source "https://rubygems.org"
gem "sinatra"
gem "json"

Out of the three lines in our Gemfile, the lines fall into two varieties. The first is the source line. This will tell bundler where to install the gems from. After that we have the gem lines. Each of these specify a gem, and optionally a version to install. With our populated Gemfile, we can run bundle install to download and install our new gems.

bundle install
Fetching gem metadata from https://rubygems.org/...........
Fetching additional metadata from https://rubygems.org/..
Resolving dependencies...
Using json 1.8.1
Installing rack 1.5.2
Installing rack-protection 1.5.3
Installing tilt 1.4.1
Installing sinatra 1.4.5
Using bundler 1.6.2
Your bundle is complete!
Use `bundle show [gemname]` to see where a bundled gem is installed.

Great. With Sinatra installed, we can continue to create our first simple web services.

Hello World!

We're going to need one more file in order to get our services setup. Create and name this one app.rb. With it created, we can have our first set of services running with only five lines. 1

require 'sinatra'

get '/' do
  "Hello World"
end

Now run ruby app.rb. At this point you should be able to hit http://localhost:4567 and see “Hello World”.

Our first working Hello World service

Okay, it works, but how? Line 1 is self-explanatory enough: we're loading the Sinatra gem. But what about after that? Line three is where the true magic happens. Sinatra has defined a number of new methods for us, including post, patch, delete, option, and among others, get. On line three we're invoking the method get with two parameters: a string ‘/', and a Proc2. Effectively, we're associating a path with an anonymous function. When we navigate to “/", Sinatra runs our Proc and displays the output to us.

So far, so good. Let's increase the scope of our hello world a bit.

A Slightly Bigger Hello World

require 'sinatra'

get '/users' do
  "Hello World"
end

post '/users' do
  [403, params[:name]]
end

Our new Hello World isn't much bigger, but it is now big enough to show off several concepts for us. For starters, we've now seen “post”, our first verb other than “get”. As you probably suspect, this associates a post to /users with the given Proc. Line seven has a second detail for us as well: /users is defined twice, with different verbs. Routes evaluated in the order that they were defined. First match wins.

There are two more things worth noting in our example. First, is the return value on line eight. The new return value is an array that follows the format [http_status, message_body]. Since our other examples returned a 200 status code, we were able to ignore explicitly returning it. The last item of interest is the params array. When a request posts data, the params array will contain all of the values sent to the server.

In the case of our example, posting to /users with a form item named ‘name’ will result in a 403 status, and a message body that matches the value we posted.

A Stubbed Service

Now we've got JUST enough Sinatra knowledge under our belt to create a dummy payment API that we can use. Our api will expect a post to /payment that contains a number and an expiration date. Invalid requests get a 400. If our card is valid, we'll get a 200 and a JSON success, otherwise, we'll get a 403 and a JSON failure message.

require 'sinatra'
require 'json'

post '/payment' do
  return [400, "Missing Card Number"] unless params[:number]
  return [400, "Missing Expiration"] unless params[:expiration]

  valid = is_valid?(params[:number], params[:expiration])
  status = valid ? 200 : 403

  [status, { "success" => valid }.to_json]
end

def is_valid?(number, expiration)
  number == "0000-0000-0000-0000" and expiration == "10/15"
end

And to test out our fake services, we'll make a few calls to it.

curl http://localhost:4567/payment -d "number=0000-0000-0000-0000;expiration=10/16" -w " %{http_code}"
{"success":false} 403
curl http://localhost:4567/payment -d "number=0000-0000-0000-0000;expiration=10/15" -w " %{http_code}"
{"success":true} 200
curl http://localhost:4567/payment -d "number=0000-0000-0000-0000" -w " %{http_code}"
Missing Expiration 400

Excellent.

Conclusion

Our final example is still a bit small, but I believe that it gives a proper idea of the kinds of items that we can easily stub out. Ideally we'd have guaranteed access to all the services we need at all stages of development. That doesn't always happen though. Still, with the knowledge of using stubbed services in our toolkit, lack of availability might not be the blocker that it once was. 3

Footnotes


  1. Technically we could condensed our sample down to two lines, if we were to sacrifice all semblance of readability. Let's not. ↩︎

  2. For my non-ruby friends, a Proc is conceptually similar to an anonymous method. Similar. ↩︎

  3. Granted, if one of the problems is that you can't access the APIs you need, I suspect there will be other problems that Ruby might not be able to help… ↩︎