Mocking Models

Posted by kev Sat, 02 Sep 2006 23:30:00 GMT

Today I decided to see how much of the test code in my current side project I could rip fixtures out of. At the same time I could see what kind of speed increase I got from staying away from the database. Model tests seem to be the most straight forward so I started there.

My assertion tests looked like this initially:

require File.dirname(__FILE__) + '/../test_helper'

class AssertionTest < Test::Unit::TestCase
  fixtures :assertions

  def test_validations
    assert_validates :presence_of, Assertion, :name, :description, :code

    # Uniqueness
    a = Assertion.new(:name => assertions(:assert_difference).name,
                      :description => 'some descr', :code => 'def assert_difference')
    assert !a.valid?
    assert_equal 'has already been taken', a.errors.on(:name), "does not validate uniqueness of name"
  end

  def test_to_ruby
    a = assertions(:assert_difference)
    code = a.to_ruby
    assert_match a.code, code
    assert_match "# #{a.name}", code
    assert_match "# #{a.description}", code
  end

  def test_to_ruby_with_multiline_name_or_description
    name = "multiline\nname\nand\ndescription"
    description = "isn't\nthis\n\awesome"

    a = Assertion.new(:name => name, :description => description)

    code = a.to_ruby
    [name.split("\n"), description.split("\n")].flatten.each do |line|
      assert_match "# #{line}", code
    end
  end

  def test_to_helper_file
    test_helper = Assertion.to_helper
    Assertion.find(:all).each do |a|
      assert_match a.to_ruby, test_helper
    end
  end
end

The first place I went to optimize was db calls in the tests that I just didn’t need. One that jumps out at me is the first line of the test_to_ruby method. We load a fixture from the database in order to test the to_ruby method on it. We don’t really need to hit the database because I can just as easily create an Assertion object in memory. Before we make the change let’s benchmark our tests:

mini:~/code/projects/assertions kev$ ruby test/unit/assertion_test.rb 
Loaded suite test/unit/assertion_test
Started
....
Finished in 0.276884 seconds.

4 tests, 21 assertions, 0 failures, 0 errors

0.276 seconds. Quite fast, but it could probably find some improvement. Let’s replace the fixture accessor call with a new Assertion object:

def test_to_ruby
  a = Assertion.new(:name => 'assert_difference', :code => 'def assert_difference; end',
                    :description => 'Does nothing')
  code = a.to_ruby
  assert_match a.code, code
  assert_match "# #{a.name}", code
  assert_match "# #{a.description}", code
end

Not much of a change. If we’re going to do this in multiple places we’ll probably extract to a helper. In the meantime let’s see how much that’s improved our runtime.

mini:~/code/projects/assertions kev$ ruby test/unit/assertion_test.rb 
Loaded suite test/unit/assertion_test
Started
....
Finished in 0.085474 seconds.

4 tests, 21 assertions, 0 failures, 0 errors

Wow, that’s about 3x faster. This isn’t any sort of uber-thorough scientifically measured increase, but we can certainly see that there is a benefit when it comes to speed. Let’s see if we can make other improvements.

The next spot we can probably improve is the test_to_helper_file method. Look, we make a .find(:all) call right there in the test itself. We also make that call to Assertion.to_helper which hits the database. Let’s look at the to_helper method for a moment.

  def self.to_helper
    <<-CODE
# Save this file to lib/ and remember to include TharBeAssertions in your test_helper.rb

module TharBeAssertions

#{self.find(:all).collect(&:to_ruby).join("\n")}

end
    CODE
  end

We can see here that when we make the to_helper call the method also calls find(:all). Notice that we take the results of the find call and collect the returned value of the to_ruby method. This can be worked with.

First we require Mocha and Stubba:

require File.dirname(__FILE__) + '/../test_helper'
require 'mocha'
require 'stubba'

class AssertionTest < Test::Unit::TestCase

Next we want to deal with that find call in to_helper. We need find to return objects that will respond to a to_ruby call and return something we can work with. For this we turn to stub objects.

Stubs are methods that return a canned value. They’re different than mocks in that they don’t verify anything, they simply give dummy information. We’ll use Stubba’s stub method to create our objects:

def test_to_helper_file
  assertions = [stub(:to_ruby => 'def some_assertion; end'), 
                stub(:to_ruby => 'def another_assertion; end')]

  test_helper = Assertion.to_helper
  Assertion.find(:all).each do |a|
    assert_match a.to_ruby, test_helper
  end
end

stub takes a hash with the form :method_name => returned_value. In our case we create objects that return ‘def some_assertion; end’ and ‘def another_assertion; end’ when to_ruby is called on them.

Next we need to make the find call in to_helper return what we’d like. We’ll do this with a mock (which will return our array of stubbed objects).

def test_to_helper_file
  assertions = [stub(:to_ruby => 'def some_assertion; end'), 
                stub(:to_ruby => 'def another_assertion; end')]
  Assertion.expects(:find).with(:all).returns(assertions)
  test_helper = Assertion.to_helper
  Assertion.find(:all).each do |a|
    assert_match a.to_ruby, test_helper
  end
end

Line 3 of the method is where we’ve injected our mock. If we read it in English it says that the Assertion class expects a call to find with the argument :all and then returns our assertions object (an array of stubs). At that point collect will be called on our array of stubs (in the to_helper method) and we’ll have our fake data in place.

If we run the code at this point we see a failure.

mini:~/code/projects/assertions kev$ ruby test/unit/assertion_test.rb 
Loaded suite test/unit/assertion_test
Started
F...
Finished in 0.019647 seconds.

  1) Failure:
test_to_helper_file(AssertionTest) [test/unit/assertion_test.rb:42]:
:find(:all): expected calls: 1, actual calls: 2

4 tests, 22 assertions, 1 failures, 0 errors

The mock we injected (using the expects call) is reporting that it actually recieved two calls to find(:all) instead of one. This is because we call Assertion.find(:all) in our test itself. We could modify our expectation but we wanted to eliminate as many of the database calls as we could anyway. Let’s see if we can remove that call.

If you think about it, we don’t need to find the assertions in the database and confirm that they showed up in the output of to_helper because that’s not where our test data comes from now. Now the test data is from our array of stubs, assertions. Let’s iterate through assertions instead and make sure the fake data showed up:

def test_to_helper_file
  assertions = [stub(:to_ruby => 'def some_assertion; end'), 
                stub(:to_ruby => 'def another_assertion; end')]
  Assertion.expects(:find).with(:all).returns(assertions)
  test_helper = Assertion.to_helper
  assertions.each do |a|
    assert_match a.to_ruby, test_helper
  end
end

When we run our tests we can see we’re back to passing and there’s been another speed improvement:

mini:~/code/projects/assertions kev$ ruby test/unit/assertion_test.rb 
Loaded suite test/unit/assertion_test
Started
....
Finished in 0.018426 seconds.

4 tests, 22 assertions, 0 failures, 0 errors

So, mocking and stubbing seems to be viable for atleast simple cases when working with models. I’m going to try using them with controller tests later and I’ll let you know how it goes.

Posted in , ,  | 2 comments

Comments

  1. Avatar Thorsten said about 4 hours later:

    Very nice! Thanks for taking the time to write this up! Now if only James would write some docs for the whole thing…

  2. Avatar James Mead said about 12 hours later:

    Kev: I’m glad you’re finding Mocha useful.

    Thorsten: Have you see the Mocha Quickstart article I wrote recently? Does that help at all? If not, what documentation would help most – detailed api/rdoc or higher level tutorial?

Comments are disabled