2

This is something that I thought would be straightforward but I'm having issues around testing the rack-timeout gem. I have a sinatra base class with an endpoint which does some logic.

module MyModule
  class MySinatra < Sinatra::Base
    use Rack::Timeout
    Rack::Timeout.timeout = 10

    get '/dosomething' do
       #do the normal logic.
    end
  end
end

More information on the rack-timeout gem is here. I'm trying to setup a test where I can send a request which I know will take more than a few seconds in order for it to fail.

Here is the test so far

require "test/unit"
require "mocha/setup"
require 'rack/timeout'

def test_rack_timeout_should_throw_timed_out_exception_test
  Rack::Timeout.stubs(:timeout).returns(0.0001)
  assert_raises TimeoutError do
      get "/dosomething"
  end
  Rack::Timeout.unstub
end

There are a number of ways this could be done but I am not sure how they would be implemented

  1. Override the '/dosomething' method as part of the test to {sleep 3}
  2. Do the same as above but with a stubbing or mocking library
  3. instead of using get "/dosomething" in the test, create a net::http response which will keep the request open.

Any thoughts on this would be very much appreciated.

Vega
  • 27,856
  • 27
  • 95
  • 103
Rooktone
  • 109
  • 1
  • 5
  • Whats wrong with stubbing it (just for the timeout test)? – Patrick Oscity Feb 17 '13 at 23:40
  • @padde This has already been done in the code. Timeout is set to 0.0001. Unless the test timeout is some incredibly small number EG 0.000000000000000000000001 (faster than the process can handle on a machine) then the test will always fail. Also, faster machines will handle the "/dosomething" method faster than others. So on some machines, the test would timeout (which is what we want) and on other slower machines the test would succeed. – Rooktone Feb 18 '13 at 09:29
  • Sorry, i meant whats wrong with overriding `get '/dosomething'` with a call to sleep that is at least as long as the timeout? – Patrick Oscity Feb 18 '13 at 20:52
  • @padde I have already listed this as a potential solution (number 2). However I am not sure of how it would be implemented. – Rooktone Feb 19 '13 at 13:03

1 Answers1

1

First of all your test will not actually pass, because the error is not handed through to the test. It is only raised on the server side. Luckily, rack-test provides the last_response.errors method to check whether there were errors. Therefore i would write the above test as follows:

def test_rack_timeout_should_throw_timed_out_exception
  Rack::Timeout.stubs(:timeout).returns(0.0001)

  get '/dosomething'

  assert last_response.server_error?, 'There was no server error'
  assert last_response.errors.include?('Timeout::Error'), 'No Timeout::Error raised'

  Rack::Timeout.unstub
end

Now the only thing left to do is to simulate a slow response by overriding the route. It seemed simple at first but then i realized it is not so simple at all when i got my hands on it. I fiddled around a lot and came up with this here:

class Sinatra::Base
  def self.with_fake_route method, route, body
    old_routes = routes.dup
    routes.clear
    self.send(method.to_sym, route.to_s, &body.to_proc)
    yield
    routes.merge! old_routes
  end
end

It will allow you to temporarily use only a route, within the block you pass to the method. For example now you can simulate a slow response with:

MyModule::MySinatra.with_fake_route(:get, '/dosomething', ->{ sleep 0.0002 }) do
  get '/dosomething'
end

Note that the get '/dosomething' inside the block is not the definition of the temporary route, but a method of rack-test firing a mock request. The actual override route is specified in form of arguments to with_route.

This is the best solution i could come up with but i would love to see a more elegant way to solve this.

Complete working example (ran on Ruby 1.9.3.p385):

require 'sinatra/base'
require 'rack/timeout'

module MyModule
  class MySinatra < Sinatra::Base
    use Rack::Timeout
    Rack::Timeout.timeout = 10

    get '/dosomething' do
      'foo'
    end
  end
end




require 'test/unit'
require 'rack/test'
require 'mocha/setup'

class Sinatra::Base
  def self.with_fake_route method, route, body
    old_routes = routes.dup
    routes.clear
    self.send(method.to_sym, route, &body)
    yield
    routes.merge! old_routes
  end
end

class Tests < Test::Unit::TestCase
  include Rack::Test::Methods

  def app
    MyModule::MySinatra
  end

  def test_rack_timeout_should_throw_timed_out_exception
    Rack::Timeout.stubs(:timeout).returns(0.0001)

    MyModule::MySinatra.with_fake_route(:get, '/dosomething', ->{ sleep 0.0002 }) do
      get '/dosomething'
    end

    assert last_response.server_error?, 'There was no server error'
    assert last_response.errors.include?('Timeout::Error'), 'No Timeout::Error raised'

    Rack::Timeout.unstub
  end
end

produces:

1 tests, 2 assertions, 0 failures, 0 errors, 0 skips
Patrick Oscity
  • 53,604
  • 17
  • 144
  • 168