Sinatra app 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.

test_everything2.jpg

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, Rpsec 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 init 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.

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 be also 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 similiar 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.

The full source code can be found in this Github repo.

 
13
Kudos
 
13
Kudos

Now read this

Progress bar for shell apps

Downloading a file, backing up your database, or installing a package for your system are all instances of long lived processes that we encounter daily. When such a process takes a long time to finish, it is always nice to give some kind... Continue →