Testing controller instance variables with Rack::Test and Sinatra
Question
I have a Sinatra app that serves pages as read-only or editable depending on if the user is logged in.
The controller sets a variable @can_edit
, that is used by the views to hide/show edit links. How can I test @can_edit
's value in my tests? I have no idea how to get at the current instance of the controller under Rack::Test.
I use class_eval
to stub the logged_in?
method in the controller, but I'm having to resort to checking last_response.body
for my edit links to see if @can_edit
has been set or not.
How can I test the value of @can_edit
directly?
Solution
Unfortunately I don't think this is possible without modifying Rack::Test. When you make a request during application testing, Rack::Test does the following:
- adds the request to a list of recent requests
- creates a new instance of your application and invokes its
call
method - adds your application's response to a list of recent responses
It's easy to access the last_request
and last_response
, but unfortunately no information is saved about the state of your application while it's running.
If you're interested in hacking together a Rack::Test patch to do this, start by looking at rack-test/lib/rack/mock_session.rb
on line 30. This is where Rack::Test runs your application and receives the standard Rack app return values (status, headers, body). My guess is that you're going to have to modify your application as well, to collect and make accessible all of its instance variables.
In any case, it's best to test for results, not implementation details. If you want to make sure an edit link is not visible, test for the presence of the edit link by DOM id:
assert last_response.body.match(/<a href="..." id="...">/)
OTHER TIPS
It is possible with a little hack. Sinatra app's instances are unavailable because they are created when Sinatra::Base#call is called. as Alex explained. This hack prepares an instance on ahead and let the next call grab it.
require 'something/to/be/required'
class Sinatra::Base
@@prepared = nil
def self.onion_core
onion = prototype
loop do
onion = onion.instance_variable_get('@app')
return onion if onion.class == self || onion.nil?
end
end
def self.prepare_instance
@@prepared = onion_core
end
# Override
def call(env)
d = @@prepared || dup
@@prepared = nil
d.call!(env)
end
end
describe 'An Sinatra app' do
include Rack::Test::Methods
def app
Sinatra::Application
end
it 'prepares an app instance on ahead' do
app_instance = app.prepare_instance
get '/foo'
app_instance.instance_variable_get('@can_edit').should be_true
end
end
I figured out this technique to mock the instance that runs the current test in the first place.
Heres a nasty but viable alternative
# app.rb - sets an instance variable for all routes
before do
@foo = 'bar'
end
# spec.rb
it 'sets an instance variable via before filter' do
my_app = MySinatraApplication
expected_value = nil
# define a fake route
my_app.get '/before-filter-test' do
# as previously stated, Sinatra app instance isn't avaiable until #call is performed
expected_value = @foo
end
my_app.new.call({
'REQUEST_METHOD' => 'GET',
'PATH_INFO' => '/before-filter-test',
'rack.input' => StringIO.new
})
expect(expected_value).to eq('bar')
end
This allows you to test against a sinatra before filter and or access instance variables created for base application.