[rspec-users] Mocks? Really?

Andy Goundry andy at adveho.net
Mon Dec 17 06:10:50 EST 2007


For me, this has certainly been the most enjoyable and interesting
part of using RSpec - finding answers to these questions in a context
that suits the project. Of course, i am new to this, but have found an
approach that works well for my current project. However, my approach
is wide open to review and improvement and will no doubt evolve well
beyond its current scope in future.

I am still reading and re-reading Dan's previous mail regarding the
Builder pattern as it is very elegant, although i am not using now as
i feel that it could introduce a little too much overhead to maintain
the Builders. I am also considering Dan's mantra and with more RSpec
experience i'll gain a better insight into how this can work in rails.

Here's what i'm doing that works well for our project:

Mocking
* In views and controllers I always use mocks and stub out any
responses that i spot or are flagged up by autotest (yep, autotest is
ace for highlighting those methods that need stubbing)
* In models, i only use real models for the model specific to the
test. I always mock all other interacting models - so in a test for a
project with many tasks, the tasks model is mocked and then stubbed.
* I only define the expectation for any mock or real model in one
place. So, in our app, the expected definition of a Project is defined
once and that definition is used by all tests that use that object,
from views to models. It's default values can be overwritten, but the
expectation is set for all uses. More info below:

Factories
So far, I am finding that a factory class is offering a useful *glue*
between the intentionally separated unit tests. So, even though all
tests are isolated from each other with mocks, they still share an
expectation of what any used mock should look like. This enables me to
be aware of the system wide impact of a change to a small component of
the system. I fully accept that this should be covered by integration
testing and not unit testing, but on a quick project, i am not sure i
can justify (not yet at least) the time to write unit tests and then
integration tests, especially as the test team will go at the app with
Scellenium. As i say, mine is an evolving platform :-)

Here's how we are using a factory. I hope it helps and that i don't
get too grilled for the design and implementation :-)

###
# The factory class houses the expected
# definition of each object and returns
# mocks or real models depending on the request
# It's attribute values (but not keys) can be overwritten
# See 'validate_attributes' method below
###

module Factory
  def self.create_project(attributes = {}, mock = false)
    @default_attributes = {
      :name => "Mock Project",
      :synopsis => "Mock Project Synopsis"
    }
    create_object(attributes,mock,Project)
  end

  private

  def self.create_object(custom_attributes,mock,object_type)
    validate_attributes(custom_attributes)
    attributes = @default_attributes.merge(custom_attributes)
    if mock
      attributes.each_pair do |key, value|
        mock.stub!(key).and_return(value)
      end
      mock
    else
      mock = object_type.create attributes
    end
  end

  ###
  # The following method validates that any received
  # custom attribute's key is in the expected attribute
  # list for the object. If not, the test fails, forcing
  # the developer to keep the factory defaults up to
  # date with any changes
  ###
  def self.validate_attributes(attributes)
    attributes.each_key {|a| raise "Unrecognised attribute '#{a}' was
passed into the Factory" if !@default_attributes.has_key?(a)}
    true
  end
end

###
# Projects controller test interacts
# with the Factory and receives mocks
###

describe ProjectsController do
  include Factory
  before(:each) do
    @project1 = Factory.create_project({}, mock_model(Project)),
    @project2 = Factory.create_project({:name => "My second project",
:synopsis => "This is another fantastic project"},
mock_model(Project))
    @projects = [@project1, at project2]
  end
end

###
# The Project model test interacts with
# the Factory and receives real models
###

describe Project do
  include Factory

  before(:each) do
    Project.destroy_all
    #Real project
    @project = Factory.create_project
    #Stub Role
    @role = Factory.create_role({},mock_model(Role))
    @role.stub!(:quoted_id).and_return(true)
    @role.stub!(:[]=).and_return(true)
    @role.stub!(:save).and_return(true)
  end
end


More information about the rspec-users mailing list