gateway drug testing
Courtenay : February 16th, 2007
For those of you still resolute in your test::unit ways, refusing, unable, or unwilling to join the dark side and move to the one true behavioral-based testing framework, here's a gateway into our world. Hopefully this guide will help you see ruby more as a message passing language, and will placate those monkeys in the trees screaming, "You don't need rpsec to mock and stub!"
I'm working on a project that uses test::unit and one of the tests looks something like this:
def test_should_not_get_edit_for_candidated_patch
login_as :quentin
get :edit, :id => 1
assert_equal "Patch is no longer editable!", flash[:notice]
assert_response :redirect
end
Should be fairly familiar to you all. Using authenticated system, and login_as sets the request.session[:user].
This is fine, but it requires you to load up the users.yml and fiddle around with various flags; you see, there's a test in the controller for current_user.administrator? but you wouldn't know that from looking at the test.
Here's the controller action we're trying to test:
def edit
unless current_user.administrator? or (@patch.opened? and @patch.belongs_to?(current_user))
flash[:notice] = "Patch is no longer editable!"
redirect_to :action => 'show'
end
end
The auth system does something like this in a filter to find the current user:
@current_user = User.find(session[:user])
return @current_user.is_a?User
It's a bit more complex than that, but basically, if the user can be found, load it up, otherwise return nil and redirect to a login page.
rewrite that test!
This is where a mock (with stubbed methods) comes in handy. First, set a dummy session variable, to keep auth_system happy (we're storing IDs not objects).
@request.session[:user] = 1
Now, the app will try to load a User object from the db, but we don't want that, do we? Why test the db from the controller? It should already be tested at the model. So, let's make a mock object
@user = mock('user')
and stub the find_by_id method, so the User model doesn't have to hit the database
User.expects(:find_by_id).with(1).returns(@user)
Or, if you're lazy like me
User.expects(:find_by_id).with(1).returns @user = mock('user')
Now, we have a virgin, impressionable object. Let's slap it round a bit.
@user.expects(:is_a?).with(User).returns(true)
Gasp! overriding the core ruby! This is getting fun..
@user.expects(:administrator?).returns(false)
Now, run that, and it warns that
#<Mock:user>.is_a? - expected calls: 1, actual calls: 4
This is because we're calling is_a?User multiple times (probably bad code in there, but regardless...)
@user.expects(:is_a?).with(User).at_least(1).returns(true)
Quick hack to tell it that the is_a?User message should happen at least once (duh!)
the code
Here's what we've built:
def test_should_not_get_edit_for_candidated_patch
@request.session[:user] = 1
User.expects(:find_by_id).with(1).returns @user = mock()
@user.expects(:is_a?).with(User).at_least(1).returns true
@user.expects(:administrator?).at_least(2).returns(false)
get :edit, :id => 1
assert_equal "Patch is no longer editable!", flash[:notice]
assert_response :redirect
end
Sweet! But you're going to want that mock stuff in other methods, too, and it's not really relevant just to this test
def setup
@request.session[:user] = 1
User.expects(:find_by_id).with(1).returns @user = mock()
@user.expects(:is_a?).with(User).at_least(1).returns true
end
def test_should_not_get_edit_for_candidated_patch
@user.expects(:administrator?).at_least(2).returns(false)
get :edit, :id => 1
assert_equal "Patch is no longer editable!", flash[:notice]
assert_response :redirect
end
So, now, obviously, we're testing the logic around a user being an administrator. No fixtures, no database required for the user. You know exactly what you're testing, and it's hell fast. Score!
4 Responses to “gateway drug testing”
Sorry, comments are closed for this article.
February 18th, 2007 at 05:47 PM
And how much does that really save you? I’ve done things like this and found it doesn’t affect the test time much at all. User.find_by_id(1) is already hella fast.
oh, and since we care about milliseconds, I recently swapped a ruby lib from rspec to test/spec and noticed the tests ran twice as fast. Course, we’re talking less than a hundredth of a second difference, but still :)
Anyways, my point is don’t worry about mocking some part of your tests unless it really matters.
February 18th, 2007 at 06:47 PM
He’s not doing it to save time. He’s doing it to avoid having to set up fixtures or populate the database just to test this controller.
February 20th, 2007 at 02:13 AM
Great article! Thanks for the heads up.
March 1st, 2007 at 12:50 AM
Hi,
You should really mention that people need to install the “mocha”:http://mocha.rubyforge.org/ plugin for this to work.