sinatra snippets

How to test a Sinatra webapp with Rspec

Tagged sinatra, rspec, spec, testing, bdd  Languages ruby

Create spec/app_spec.rb and put the following in it:

require 'sinatra'
require 'spec/interop/test'
require 'sinatra/test/unit'

require 'app'
require 'test/spec'

set :environment, :test

describe 'The HelloWorld App' do
  before(:each) do
    set :views => File.join(File.dirname(__FILE__), "..", "views")
  end

  it "says hello" do
    get '/'
    response.should.be.ok
    response.body.should.equal 'Hello World'
  end
end

Then run the test:

spec spec/app_spec.rb

Testing sessions with Sinatra and Rack::Test

Tagged session, test, sinatra, rack, cookie, rack::test, mocha  Languages ruby

You need support for testing sessions when, for example, testing authentication. The only way I've managed to get sessions to work with Sinatra and Rack::Test is by going through the whole stack, in other words calling the authentication controller as shown here:

@user = Factory(:user) # create a dummy user
User.expects(:authenticate).with(any_parameters).returns(@user) # make authenticate return the dummy user
post "/sign-in", {:email => @user.email, :password => @user.password} # login to the application and set a session variable.

After this the session populated and we're logged in when accessing the application again:

post "/articles", {:title => 'Sessions suck', :body => '...'}

On a side note, there are two ways of specifying sessions that used to work, but which no longer work with Sinatra 1.0:

get '/', :env => { :session => {:abc => 'adf'} }
get '/', {}, :session => {:abc => 'adf'}
get '/', {}, "rack.session" => {:abc => 'adf'}

The session is always empty.

There are many discussions about sessions and Rack::Test, but not one of them has a solution that works for me: * rack.session variable is missing env under test environment * Sessions with rspec and Rack.Test

My Sinatra+Capistrano+Capinatra deployment recipe

Tagged capistrano, sinatra, capinatra  Languages ruby
require 'capistrano/version'
require 'rubygems'
require 'capinatra'
load 'deploy' if respond_to?(:namespace) # cap2 differentiator

# set an app_class if you're using the more recent style of creating
# Sinatra apps, where app_class would be the name of your subclass
# of Sinatra::Base. if you're just requiring 'sinatra' and using the
# more traditional DSL style of Sinatra, then comment this line out.
set :app_class, 'xxx'

# standard settings
set :app_file, "xxx.rb"
set :application, "xxx"
set :domain, "xxx.com"
role :app, domain
role :web, domain
role :db,  domain, :primary => true

set :ssh_options, { :forward_agent => true }

#set :use_sudo, false

# environment settings
set :user, "xxx"
set :group, "www-data"
set :deploy_to, "/var/www/#{application}"
set :deploy_via, :copy #:remote_cache
default_run_options[:pty] = true

# scm settings
set :repository, "git@xxx.com:xxx.git"
#set :repository, "file:///home/git/repositories/xxx.git"
set :scm, "git"
set :branch, "master"
set :git_enable_submodules, 1

# where the apache vhost will be generated
set :apache_vhost_dir, "/etc/apache2/sites-enabled/"

namespace :deploy do
  task :restart do
    run "touch #{current_path}/tmp/restart.txt"
  end
end

How to create a JSONP cross-domain webservice with Sinatra and Ruby

Tagged json, jsonp, sinatra, jquery  Languages ruby

Your Sinatra app:

get '/' do
    callback = params.delete('callback') # jsonp
    json = {'your' => 'data'}.to_json

    if callback
      content_type :js
      response = "#{callback}(#{json})" 
    else
      content_type :json
      response = json
    end
    response
  end

Your HTML:

<script type="text/javascript">
    function parseResponse(json) {
    // Do something with the data
    }
    </script>
    <script type="text/javascript" src="http://xxx.com/?callback=parseResponse"></script>

You can also do the same with jQuery:

$.ajax({
    type: 'get',
    url: '/',
    dataType: 'jsonp',
    success: function(data) {
      parseResponse(data);
    }
})

How to test modular Sinatra apps with Rack::Test

Tagged sinatra, rack::test, urlmap  Languages ruby

Let's say you have this in your config.ru:

run Rack::URLMap.new \
  "/" => HomeController.new,
  "/user" => UserController.new

and you want to test both / and /user at the same time.

The solution is to return an instance of Rack::Builder instead of, for example, HomeController, which this snippet does by reading config.ru and evaluating it:

def app
  eval "Rack::Builder.new {( " + File.read(File.dirname(__FILE__) + '/../config.ru') + "\n )}"
end

The code was found on the internets and might not work if Sinatra/Rack is changed.

How to test one controller in isolation

To test just one controller use this code:

def app
    Rack::Builder.new do
      run ProductsController
    end
end

or even simpler:

def app
    ProductsController
end

Autotest for Sinatra

Tagged watchr, autotest, sinatra, guard  Languages ruby

Guard

Add this to a file named Guardfile in your app directory:

# gem install guard guard-minitest
# https://github.com/guard/guard#readme

guard 'minitest' do
  watch(%r{^app/(.+)\.rb$})   { |m| "test/app/#{m[1]}_test.rb" }
  watch(%r{^lib/(.+)\.rb$})   { |m| "test/lib/#{m[1]}_test.rb" }
  watch(%r{^test/(.+)\.rb$})  { |m| "test/#{m[1]}.rb" }
end

Watchr

Put this in test/test.watchr (inspired by Padrino):

#
# From https://raw.github.com/padrino/padrino-recipes/master/files/watchrs/test.watchr
#
# install watchr
# $ gem install watchr
#
# Run With:
# $ watchr test.watchr
#

# --------------------------------------------------
# Helpers
# --------------------------------------------------

def run(cmd)
  exec = "bundle exec ruby #{cmd}"
  puts(exec)
  system(exec)
end
 
def run_all 
  system("bundle exec rake test")
end

# --------------------------------------------------
# Watchr Rules
# --------------------------------------------------
watch("^lib/(.*)\.rb")                     { |m| run("test/lib/#{m[1]}_test.rb") }

watch("^(.*)/controllers/(.*).rb")           { |m| run("test/#{m[1]}/controllers/#{m[2]}_controller_test.rb") }
watch("^test/(.*)/controllers/(.*)_test.rb") { |m| run("test/#{m[1]}/controllers/#{m[2]}_test.rb") }

watch("^(.*)/models/(.*).rb")                { |m| run("test/#{m[1]}/models/#{m[2]}_test.rb") }
watch("^test/(.*)/(.*)_test.rb")             { |m| run("test/#{m[1]}/models/#{m[2]}_test.rb") }

watch("test.*/test_helper\.rb")              { run_all }
watch("^test/(.*)_test\.rb")                 { |m| run("test/#{m[1]}_test.rb") }

# --------------------------------------------------
# Signal Handling
# --------------------------------------------------
# Ctrl-\ or Ctrl-ö (Swedish keyboard)
Signal.trap('QUIT') do
  puts "Quitting..."
  abort("\n") 
end

# Ctrl-C
Signal.trap('INT') do 
  puts " --- Running all tests ---\n\n"
  run_all
end

# Find your signals...
#Signal.list.each do |name, num|
#  Signal.trap(num) do 
#    puts "#{name}"
#  end rescue "xx"
#end

Put this in .bash_profile or .zsh/aliases:

alias autotest='watchr test/test.watchr'

Install watchr:

gem install watchr

Test your code:

exec # reload profile
autotest

You can also use Guard or autotest. Although improvements have been made during the years, they are all IMHO flawed pieces of software and badly documented.

How to Add a Console to Sinatra Applications

Tagged sinatra, pry, console  Languages ruby

In your application directory, create .pryrc:

Pry.config.editor = "gvim --nofork"

# Print Ruby version at startup
Pry.config.hooks.add_hook(:when_started, :say_hi) do
  puts "Using Ruby version #{RUBY_VERSION}"
end

# Require Sinatra application
require 'config/environment'

Now you can start the console by typing:

$ pry
# or with bundler :(
$ bundle exec pry

Sinatra App Template

Tagged sinatra, slim, pry, sprockets  Languages ruby

A quick and dirty template for Sinatra apps with many of the features you can find in Rails.

Supports: * Slim views * Sass stylesheets * Coffee script * Compass * Susy (susy.oddbird.net) * Asset pipeline (Sprockets) * View helpers * A console * I18n * Logging * ActiveRecord+migrations * Debugging with Pry * Flash messages * Class reloading in development mode * Security features such as CSRF, Cross Site Scripting, etc. All provided by Rack::Protection and Sinatra.

Create app.rb:

require 'slim'
require 'i18n'
require 'mysql2'
require 'sprockets'
require 'sprockets-sass'
require 'compass'
require 'susy'
require 'sinatra/base'
require 'sinatra/cookies'
require 'sinatra/content_for'
require 'sinatra/activerecord'
require 'sinatra/partial'
require 'rack-flash'

class App < Sinatra::Base
  enable :sessions, :logging
  register Sinatra::Partial

  use Rack::MethodOverride
  use Rack::Flash, :accessorize => [:info, :error, :success], :sweep => true
  use Rack::Protection::AuthenticityToken # HTML forms now require: input name="authenticity_token" value=session[:csrf] type="hidden"
  set :public_folder, File.dirname(__FILE__) + '/public'
  set :views, File.dirname(__FILE__) + '/app/views'
  set :slim, :layout_engine => :slim, :layout => :'layouts/default', :use_html_safe => true, :pretty => App.environment == :development
  set :partial_template_engine, :slim
  set :session_secret, "25729f31a6bc7c57f8575db9b79ee468...." # SecureRandom.hex(128)
  set :cookie_options, { path: '/'}

  def self.sprockets
    project_root = File.expand_path(File.dirname(__FILE__))
    assets = Sprockets::Environment.new(project_root)
    assets.append_path('app/js')
    assets.append_path('app/css')
    # Twitter Bootstrap...
    assets.append_path('lib/bootstrap/js')
    assets.append_path('lib/bootstrap/css')
    assets
  end

  helpers Sinatra::Cookies
  helpers Sinatra::ContentFor
  helpers do
    def t(*args)
      ::I18n::t(*args)
    end
    def authenticity_token
      session[:csrf] = SecureRandom.hex(128) unless session.has_key?(:csrf)
      %Q{<input type="hidden" name="authenticity_token" value="#{session[:csrf]}"/>}
    end
  end

  configure do
    # Configure logging, WTF
    set :logging, false
    class ::Logger; alias_method :write, :<<; end
    logfile = File.join(App.root, 'log', "#{App.environment}.log")
    # Send STDs to log file
    $stdout.reopen(logfile)
    $stderr.reopen(logfile)
    $stderr.sync = true
    $stdout.sync = true
    # Weekly roll
    log  = Logger.new(logfile, 'weekly')
    log.level = Logger::DEBUG
    # use Rack::CommonLogger, log
    set :log, log

    Compass.add_project_configuration(File.join(Sinatra::Application.root, 'config', 'compass.rb'))
  end

  configure :development do
    require "sinatra/reloader"
    register Sinatra::Reloader
    also_reload 'app/**/*.rb'
    also_reload 'lib/**/*.rb'
    also_reload 'conf/**/*.rb'
    set :raise_errors, true
  end

  [:error, :info, :success].each do |key|
    class_eval "
    def flash_#{key}(key, now=true)
      message(key, :#{key}, now)
    end
    "
  end

  def message(key, type=:notice, now=true)
    hash = now ? flash.now : flash
    hash[type] = I18n.t(key)
  end

  # Set view variables, e.g.:
  # - meta :title, "Page title"
  # Retrieve view variables, e.g.:
  # = meta :title
  def meta(key, value = nil)
    value ? content_for(key) { value } : yield_content(key)
  end

  # Helper method for creating HTML tags:
  # content_tag :a, 'Contact us', href: 'mailto:xxx@xxx.com'
  def content_tag(name, content, attributes = nil)
    name = html_escape(name) unless name.html_safe?
    content = html_escape(content) unless content.html_safe?
    attributes = attributes.map do |name, value|
      value = html_escape(value) unless value.html_safe?
      %Q{#{name}="#{value}"}
    end if attributes && attributes.any?
    start = [name, attributes.join(" ")].reject(&:nil?).join(' ')
    "<#{start}>#{content}</#{name}>"
  end

  def debug_something_with_pry
    Kernel.binding.pry
  end

  error do
    slim :'errors/500'
  end

  not_found do
    slim :'errors/404'
  end

  error ActiveRecord::RecordNotFound do
    slim :'errors/404'
  end

  before do
    I18n.locale = params[:locale] || I18n.default_locale
  end
end

# Require attr_accessible...
ActiveRecord::Base.send(:attr_accessible, nil)

# Move to config/init/db.rb if you like
OpenStruct.new(YAML::load(File.open('config/database.yml'))[App.environment.to_s].symbolize_keys).tap do |config|
  ActiveRecord::Base.establish_connection(
    host: config.host,
    adapter: config.adapter,
    database: config.database,
    username: config.username,
    password: config.password
  )
end

%w(models controllers concerns).each do |name|
  Dir[File.join('app', name, '**/*.rb')].each do |file|
    require_relative file
  end
end

# Move to config/init/i18n.rb if you like
Dir[File.join(App.root, 'config', 'locales', '*.yml')].each do |file|
  I18n.backend.load_translations(file)
end
I18n.default_locale = :en

# Move to app/controllers/root_controller.rb
class RootController < App
  get '/' do
    flash_alert "Thanks for nothing"
    slim :'index'
  end
end

Create Gemfile:

source :rubygems
gem 'thin'
gem 'slim'
gem 'sass'
gem 'sinatra'
gem 'sprockets'
gem 'sprockets-sass'
gem 'compass'
gem 'susy'

gem 'sinatra-partial'
gem 'coffee-script'
gem 'therubyracer'
group :development do
  gem 'sinatra-reloader'
  gem 'sinatra-activerecord'
  gem 'pry'
end
gem 'rack-flash3'
gem 'awesome_print'
gem 'mysql2'

Create Rakefile:

require 'bundler/setup'
require 'sinatra/activerecord/rake'
require 'pry'
require './app'

Dir[File.join('lib', 'tasks', '**', '*.rake')].each do |file|
  import file
end

task :console do
  binding.pry
end

# Asset pipeline (Sprockets)
namespace :assets do
  task :precompile do
    App.sprockets['application.js'].write_to('public/assets/application.js')
    App.sprockets['application.css'].write_to('public/assets/application.css')
  end
end

Create config/database.yml:

development:
  adapter: mysql2
  database: xxx_development
  username: root
  password: 
  host: localhost

Create config.ru:

require './app'
map "/" do
  run RootController
end

map "/assets" do
 run App.sprockets
end

Run:

mkdir -p app/controllers app/models app/concerns app/views/layouts app/css app/js lib tmp log public/img config/init db/migrate lib/tasks
bundle
bundle exec thin start -p 3000

How to test Sinatra helpers

Tagged request, sinatra, test  Languages ruby

This one depends on a request object (Rack::Request) being present:

class AppHelpersSpec < Test::Unit::TestCase                                            
  include AppHelpers
  include Rack::Test::Methods

  def request
    Rack::Request.new(Rack::MockRequest.env_for '/', {params: {name: 'hohohoe'}})
  end

  def test_helper_method
    res = some_helper_method # call helper method
    assert_equal 'WTF!', res
  end
end

LOL.