[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