Home · Blog · Talks · Projects

Mock Commands, Stub Queries

Zach Moazeni has just posted a suggested patch for Mocha over on his blog. My understanding of the patch is that it means expectations are verified even when an assertion error occurs in the test. Here is his example…

class Car

  def initialize(parts = [])
    @parts = parts
  end
  
  def start
    started = true
    @parts.each do | part |
      # commenting out for failure
      # started = started && part.start
    end
    
    started
  end

end
class SomeTest < Test::Unit::TestCase

  def test_start
    engine_mock = mock("engine_mock")
    car = Car.new([engine_mock])
    
    engine_mock.expects(:start).returns(false)
    assert !car.start
  end

end

I’ve had a friendly & useful conversation with Zach about it, but I’m not convinced this is the right way to go. Using the one assertion per test school of thought, you can achieve the same goal by splitting the test into two so you get a test failure for the expectation and another for the assertion…

class SomeTest < Test::Unit::TestCase

  def test_should_start_engine
    engine = mock('engine')
    car = Car.new([engine])
    
    engine.expects(:start)
    
    car.start
  end
  
  def test_should_start_if_engine_starts
    engine = stub('engine')
    car = Car.new([engine])
    
    engine.stubs(:start).returns(false)
    
    assert !car.start
  end

end

Something that makes the example less suitable for mocking is that the Car#start method is both a command and a query. If you separate the two, testing with mocks might be easier…

class Car

  def initialize(parts = [])
    @parts = parts
  end
  
  def start
    @parts.each { |part| part.start }
  end
  
  def started?
    @parts.all? { |part| part.started? }
  end

end
class SomeOtherTest < Test::Unit::TestCase

  def test_should_start_engine
    engine = mock('engine')
    car = Car.new([engine])
    
    engine.expects(:start)
    
    car.start
  end
  
  def test_should_not_be_started_if_engine_is_started
    engine = stub('engine')
    car = Car.new([engine])
    
    engine.stubs(:started?).returns(false)
    
    assert !car.started?
  end

end

I’d be interested to know what other people think…

One thing I do agree with Zach about is that submitting a suggested patch to an open source project is a great way of initiating a constructive conversation.

Contact · History · Colophon · Links