Igor Šarčević

Sinatra applications with RSpec

There are times when the only thing I want to create is a simple API. In the Ruby ecosystem the standard for creating something simple is Sinatra. But, there are a lot of things you miss in Sinatra that you have predefined in Rails. Sinatra let’s you define and include only the things you actually need. Maybe this is a good thing, maybe it is not.

However, the things you need for a complete Sinatra application are sometimes hard to include. This tutorial aims to help you in that process, by showing you how to create a simple Sinatra web application that uses RSpec for testing.

We will create a simple Todo application with only two actions. One for listing all the existing Todos and another one for adding a new one. It will use Sinatra for creating the API, RSpec for testing, and it will also include Active Support for adding a couple of nice functionality to the Ruby language.

Bootstrap

So let’s begin! The first thing is to create a Gemfile and list all the dependencies. From the above description of the project we can simply deduct all the things that are necessary.

source "https://rubygems.org"

gem "rack"
gem "sinatra"
gem "activesupport"

group :test do
  gem "rspec"
  gem "rack-test"
end

We can install the dependencies with bundle install. I like to put my dependencies in the vendor/bundle directory. With that the command becomes the following

bundle install --path vendor/bundle

Then we can initialize RSpec in for project. The following command should create an .rspec and a spec/spec_helper.rb file.

bundle exec rspec --init

As an old Rails habit, I like to have two folders in my project. - app for containing all the application specific files. - config for the project’s configuration

Let’s create them:

mkdir app
mkdir config

Configuration

In this example project I will have only one configuration file called config/environment.rb. This file will be responsible for booting up the application and loading all the gems from Bundler. Also it will load and set up active support for this project.

Instead of using a database, this simple API will use only a global variable named $db. This is for the sake of simplicity and should be replaced with a real database.

With the above description we can implement that file like this

require "rubygems"
require "bundler"

Bundler.require(:default)                   # load all the default gems
Bundler.require(Sinatra::Base.environment)  # load all the environment specific gems

require "active_support/deprecation"
require "active_support/all"

$db = []                                    # a fake database

Now we should make the spec_helper load this file before the tests are run. To do that we need to prepend that file with a require statement.

While we are here it would also be a good idea to set the environment to test when running RSpec, include some rake test helpers, and clear the fake database before each test.

Your spec/spec_helper.rb file should look similar to this.

ENV['RACK_ENV'] = 'test'

require "./config/environment"

RSpec.configure do |config|
  config.include Rack::Test::Methods

  config.before(:each) do
    $db = []
  end 

  config.expect_with :rspec do |expectations|
    expectations.include_chain_clauses_in_custom_matcher_descriptions = true
  end 

  config.mock_with :rspec do |mocks|
    mocks.verify_partial_doubles = true
  end 

  config.filter_run :focus
  config.run_all_when_everything_filtered = true

  config.disable_monkey_patching!

  config.warnings = true

  if config.files_to_run.one?
    config.default_formatter = 'doc'
  end 

  config.profile_examples = 10

  config.order = :random

  Kernel.srand config.seed
end

The API

As a good TDD abiding citizen we should write some test for our Todo application before actually implementing the code.

Create an app/todo_api.rb and spec/app/todo_api_spec.rb file

touch app/todo_api.rb
touch spec/app/todo_api_spec.rb

and describe the application in your test file

require "spec_helper"

RSpec.describe TodoApi do
  def app
    TodoApi # this defines the active application for this test
  end
end

and then a matching class in the app file

class TodoApi < Sinatra::Base

end

Then we should create a get action for listing all the todos in the system. Such a get request should return nothing if the database is empty and a list of todos if the contains several todos

describe "GET todos" do      
  context "no todos" do      
    it "returns no todo" do
      get "/"

      expect(last_response.body).to eq("")
      expect(last_response.status).to eq 200
    end
  end

  context "several todos" do
    before do
      @todos = ["hello", "world", "!"]
      $db = @todos
    end

    it "returns all the todos" do   
      get "/"

      expect(last_response.body).to eq @todos.join("\n")
      expect(last_response.status).to eq 200
    end
  end
end

A matching implementation would be

get "/" do
  $db.join("\n")
end 

Creating a new todo is also simple. The todo message should be passed as an argument to the POST action.

describe "POST todo" do
  it "returns status 200" do
    post "/", :todo => "hello rspec"

    expect(last_response.status).to eq 200
  end

  context "todo param missing" do 
    it "returns status 400" do      
      post "/"

      expect(last_response.status).to eq 400
    end
  end
end

It’s implementation

post "/" do
  return 400 unless params["todo"].present?

  $db << params["todo"]
  200 
end 

Making rack happy and running the App

In order to run your application with the rackup command we should create an config.ru file with the following content

require "./config/environment"

run Rack::URLMap.new("/" => TodoApi)

and run our application with

bundle exec rackup config.ru

Hooray! Our simple API is finished.

Read the full source code in this GitHub repository.