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”

  1. rick Says:

    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.

  2. Tom Says:

    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.

  3. Matthijs Langenberg Says:

    Great article! Thanks for the heads up.

  4. Saimon Moore Says:

    Hi,

    You should really mention that people need to install the “mocha”:http://mocha.rubyforge.org/ plugin for this to work.

    
    script/plugin install svn://rubyforge.org/var/svn/mocha/trunk
    

Sorry, comments are closed for this article.