Preview of Latest Mocha Changes

I’ve finally managed to find some time to do some serious work on Mocha. Here are some code snippets showing the new functionality available in trunk (revision 128). I don’t don’t know how many people out there are using trunk, but it would be great to get some feedback on these changes before I make a new release. In particular I’d like to know whether…

  • I’ve broken anybody’s tests.
  • Anybody has a legitimate use for parameter_block in Expectation#with that they don’t think will be handled by a suitable parameter matcher i.e. using the current behaviour where the block is passed the parameters and the result of the block determines whether the expectation matches. I’m planning on deprecating this soon.
  • Anybody has a legitimate use for passing in an instance of Proc to Expectation#returns i.e. using the current behaviour where the Proc gets executed to generate a return value. I’m planning on deprecating this soon as well.

I’ve typed this up in a bit of a rush (about to go on holiday for a few days), so apologies if there are any mistakes.

Parameter Matchers

I’ve added a few Hamcrest-style parameter matchers which are designed to be used inside Expectation#with. The following matchers are currently available: has_key(), has_value() & has_entry(). More to follow soon. The idea is eventually to get rid of the nasty parameter_block option on Expectation#with.

object = mock()
object.expects(:method).with(has_key('key_1'))
object.method('key_1' => 1, 'key_2' => 2)
# no verification error raised
object = mock()
object.expects(:method).with(has_key('key_1'))
object.method('key_2' => 2)
# verification error raised, because method was not called with Hash containing key: 'key_1'

Values Returned and Exceptions Raised on Consecutive Invocations

Allow multiple calls to Expectation#returns and Expectation#raises to build up a sequence of responses to invocations on the mock. Added syntactic sugar method Expectation#then to allow more readable expectations.

object = mock()
object.stubs(:method).returns(1, 2).then.raises(Exception).then.returns(4)
object.method # => 1
object.method # => 2
object.method # => raises exception of class Exception
object.method # => 4

Yields on Consecutive Invocations

Allow multiple calls to yields on single expectation to allow yield parameters to be specified for consecutive invocations.

object = mock()
object.stubs(:method).yields(1, 2).then.yields(3)
object.method { |*values| p values } # => [1, 2]
object.method { |*values| p values } # => [3]

Multiple Yields on Single Invocation

Added Expectation#multiple_yields to allow a mocked or stubbed method to yield multiple times for a single invocation.

object = mock()
object.stubs(:method).multiple_yields([1, 2], [3])
object.method { |*values| p values } # => [1, 2] # => [3]

Invocation Dispatch

Expectations were already being matched in reverse order i.e. the most recently defined one was being found first. This is still the case, but we now stop matching an expectation when its maximum number of expected invocations is reached. c.f. JMock v1. A stub will never stop matching by default. Hopefully this means we can soon get rid of the need to pass a Proc to Expectation#returns.

object = mock()
object.stubs(:method).returns(2)
object.expects(:method).once.returns(1)
object.method # => 1
object.method # => 2
object.method # => 2
# no verification error raised

This should still work…

Time.stubs(:now).returns(Time.parse('Mon Jan 01 00:00:00 UTC 2007'))
Time.now # => Mon Jan 01 00:00:00 UTC 2007
Time.stubs(:now).returns(Time.parse('Thu Feb 01 00:00:00 UTC 2007'))
Time.now # => Thu Feb 01 00:00:00 UTC 2007

Acknowledgements

Thanks to David Chelimsky, Dan North, Jay Fields, Kevin Clark, Frederick Cheung, James Moore, Brian Helmkamp, Ben Griffiths, Chris Roos & Paul Battley for their input. Apologies to anybody I forgot to mention.