[ANN] Mockery initial release (yet another dynamic mock objectgenerator)

G

Gary Shea

Ruby is a really easy language to write mocks in, and it took
thousands of lines of test-first code before I finally got tired of
typing unit-test code like:

x_class = Class.new(Some::Klass)
x_class.class_eval {
attr_reader :blah
attr_writer :ret_val
def doit(blah)
@blah = blah
return @ret_val
end
}
x = x_class.new
ret_val = Object.new
x.ret_val = ret_val

rv = x.please_call_doit(blah)

assert_equal(blah, x.blah)
assert_equal(ret_val, rv)

There's probably one of these mock objects for every 5-10 lines of my
program code, and that adds up to a LOT of typing.

Mockery (http://rubyforge.org/projects/mockery) is a Ruby version of the
kind of dynamic mock generator that is commonplace in Java. The Java
versions tend to do a lousy job mocking classes as opposed to
interfaces, but we don't have that problem in Ruby (not only are there
no interfaces, but dynamic class modification is SO easy it's just not
an issue).

A first draft of the above example using Mockery looks like:

ctl = Mockery::Controller.new(Some::Klass)
ctl.record do |x|
x.doit('blah')
end
ctl.try do |x|
x.please_call_doit('blah')
end
assert_equal(true, ctl.validate, ctl.error_report)

This code will detect if the call to #please_call_doit really
calls #doit with the correct argument, exactly one time. #doit need not
actually exist in Some::Klass -- because it's mentioned in the #record
block, it will be mocked in the #try block.

You may be thinking that the above test fails to handle the return value
that the hand-coded example checks. Mockery can do that too. Each time
a call is made to a recording object (arguments to the #record block), a
call object is returned. The call object's #return_value= method may be
used to set a desired return value:

ctl = Mockery::Controller.new(Some::Klass)
ctl.record do |x|
call = x.doit(blah)
call.return_value = 'blork'
end
ctl.try do |x|
rv = x.please_call_doit(blah)
assert_equal('blork', rv)
end
assert_equal(true, ctl.validate, ctl.error_report)

which fully captures the test done in the coded-by-hand version. The
lines-of-code count is not vastly different, but the potential for error
is, as is the amount of thought that goes into the test. I find working
with Mockery much faster.

Finally, an arbitrary number of classes to mock may be handed to the
Mockery::Controller#new, each of which will be provided with a recorder
argument in the #record block, and a mock in the #try block.

This is a first release, so it's far short of perfect. I have tentative
plans for a number of improvements, and I'm open to suggestions. I'm
especially open to patches :)

The biggest weakness of Mockery at the moment is that it does not have a
clue about mocking methods that use a block. Let me catch my breath
before tackling that one ;)

Regards,

Gary Shea
 
P

Pit Capitain

Gary said:
Mockery (http://rubyforge.org/projects/mockery) is a Ruby version of the
kind of dynamic mock generator that is commonplace in Java. The Java
versions tend to do a lousy job mocking classes as opposed to
interfaces, but we don't have that problem in Ruby (not only are there
no interfaces, but dynamic class modification is SO easy it's just not
an issue).

A first draft of the above example using Mockery looks like:

ctl = Mockery::Controller.new(Some::Klass)
ctl.record do |x|
x.doit('blah')
end
ctl.try do |x|
x.please_call_doit('blah')
end
assert_equal(true, ctl.validate, ctl.error_report)

Nice work! I will try it if I need mocks in my unit tests. One first
question: couldn't you put the last assert into the #try method?
Something like

def try
...
ensure
assert_equal(true, validate, error_report)
end

Regards,
Pit
 
G

Gary Shea

Nice work! I will try it if I need mocks in my unit tests. One first
question: couldn't you put the last assert into the #try method?
Something like

def try
...
ensure
assert_equal(true, validate, error_report)
end

Regards,
Pit

I didn't even know you could do that, very slick. The only problem is
that the idea of the #validate and #error_report methods is to make #try
independent of the choice of test framework. So we could do what you're
saying, but we would have to create a new method:

def test_unit_try
...
ensure
assert(validate, error_report)
end

and the module that defines that method would have to
require 'test/unit'
which means a built-in dependency on Test::Unit.

Still, it _would_ be convenient to not have to do the assert manually.
A dumb-and-obvious way would be to have a subclass
Mockery::TestUnitController of Mockery::Controller that just wraps #try
as above. Unless I think of or hear a better idea I'll do that for the
next release.

Thanks!

Gary
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Similar Threads


Members online

Forum statistics

Threads
473,769
Messages
2,569,580
Members
45,053
Latest member
BrodieSola

Latest Threads

Top