[rspec-users] How to spec code with multiple (interacting) paths
Ashley Moran
work at ashleymoran.me.uk
Tue Feb 20 07:07:14 EST 2007
Hi
Code with a large number of different paths is probably the biggest
problem I have right now. I've made a sample class that illustrates
the simplest case of one parameter that takes two values:
class DataStorer
def initialize(logger, emailer, db_updater, do_update_db_step)
@logger = logger; @emailer = emailer; @db_updater = db_updater
@do_update_db_step = do_update_db_step
end
def store(data)
@logger.log("I was told to store '#{data}'")
@emailer.send_email("Somebody stored '#{data}'")
@db_updater.update_db("SET data = '#{data}' WHERE id = 1")
if @do_update_db_step
end
end
I need to specify the behaviour of my DataStorer objects when they
told to/not to update the database. Here's a full spec for the case
when we do want to update the database:
context "A DataStorer where 'do_update_db_step' is true" do
setup do
@logger = mock("logger"); @logger.stub!(:log)
@emailer = mock("emailer"); @emailer.stub!(:send_email)
@db_updater = mock("db_updater"); @db_updater.stub!
(:update_db)
@data_storer = DataStorer.new(@logger, @emailer,
@db_updater, true)
end
specify "should log the data given when asked to store" do
@logger.should_receive(:log).with("I was told to store 'moo'")
@data_storer.store("moo")
end
specify "should email the data given when asked to store" do
@emailer.should_receive(:send_email).with("Somebody stored
'moo'")
@data_storer.store("moo")
end
specify "should store the data given to the DB when asked to
store" do
@db_updater.should_receive(:update_db).with("SET data =
'moo' WHERE id = 1")
@data_storer.store("moo")
end
end
Now the problem is we don't know what it should do when we tell it to
NOT update the database. Here are things I've done in the past.
First, a separate spec for the differences in behaviour.
# Solution 1 - separate spec for differences
context "S1 A DataStorer where 'do_update_db_step' is false" do
setup do
@logger = mock("logger"); @logger.stub!(:log)
@emailer = mock("emailer"); @emailer.stub!(:send_email)
@db_updater = mock("db_updater"); @db_updater.stub!
(:update_db)
@data_storer = DataStorer.new(@logger, @emailer,
@db_updater, false)
end
specify "should NOT store the data given to the DB when asked
to store" do
@db_updater.should_not_receive(:update_db)
@data_storer.store("moo")
end
end
I have a bad feeling about this. If I change the emailer line in the
store method to this:
@emailer.send_email("Somebody stored '#{data}'") if
@do_update_db_step
The behaviour has changed and the specs still pass. This seems bad.
Next try, duplicate the entire spec and change the relevant bits:
# Solution 2 - copy, paste, update
context "A DataStorer where 'do_update_db_step' is false" do
setup do
# ...
end
specify "should log the data given when asked to store" do
# ...
end
specify "should email the data given when asked to store" do
# ...
end
specify "should NOT store the data given to the DB when asked
to store" do
# ...
end
end
Great for a boolean, not so hot when 6 different things can change -
you end up with 7 times the code of the single spec. Probably a very
bad idea with two parameters that can each take significant 10
values. This violates DRY. I don't know if that's a priority in
specs or not. I'm sure code coverage is more important, but it
quickly gets out of hand.
Next attempt - generate the specs in a loop. I've done this before,
and the exact structure of the contexts and specs depend on exactly
what the parameters do. Some times I've been tempted to do this, but
the difference in specs are significant enough, and the number of
variants small enough, that copy-paste-update seems better.
# Solution 3 - dynamic contexts and specs
[true, false].each do |do_update_db_step|
context "A DataStorer where 'do_update_db_step' is #
{do_update_db_step}" do
setup do
@logger = mock("logger"); @logger.stub!(:log)
@emailer = mock("emailer"); @emailer.stub!
(:send_email)
@db_updater = mock("db_updater"); @db_updater.stub!
(:update_db)
@data_storer = DataStorer.new(@logger, @emailer,
@db_updater, do_update_db_step)
end
specify "should log the data given when asked to store" do
@logger.should_receive(:log).with("I was told to store
'moo'")
@data_storer.store("moo")
end
specify "should email the data given when asked to store" do
@emailer.should_receive(:send_email).with("Somebody stored
'moo'")
@data_storer.store("moo")
end
case do_update_db_step
when true
specify "should store the data given to the DB when
asked to store" do
@db_updater.should_receive(:update_db).with("SET data
= 'moo' WHERE id = 1")
@data_storer.store("moo")
end
when false
specify "should NOT store the data given to the DB when
asked to store" do
@db_updater.should_not_receive(:update_db)
@data_storer.store("moo")
end
end
end
end
This works well if you like reading the formatted output (eg in
TextMate). Unfortunately it makes the spec code a lot harder to read.
I can't find an ideal solution to this situation, but unfortunately
it crops up quite often for me. Has anyone got any experiences or
opinions that will help?
Cheers
Ashley
More information about the rspec-users
mailing list