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,@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