A mock which extends rather than replaces a class?

While running tests, we would like to instrument some of the classes
under test. We still want the classes to do exactly what they currently
do, but we would like them to do more when running in the test
environment.

Clearly we can (and currently do) just extend the objects “on the fly”
where necessary, but this is a bit messy - we’d like some centralised
way to always extend the objects for all tests. Is there any way of
using the test/mocks directory to achieve this? We can’t see any way to
use it to do anything other than completely replace a class.

Thanks in advance!

paul.butcher->msgCount++

Paul B. wrote:

While running tests, we would like to instrument some of the classes
under test. We still want the classes to do exactly what they currently
do, but we would like them to do more when running in the test
environment.

Clearly we can (and currently do) just extend the objects “on the fly”
where necessary, but this is a bit messy - we’d like some centralised
way to always extend the objects for all tests. Is there any way of
using the test/mocks directory to achieve this? We can’t see any way to
use it to do anything other than completely replace a class.

Thanks in advance!

paul.butcher->msgCount++

Have you considered Stubba? It comes with the Mocha library.

http://mocha.rubyforge.org/

Luke R. wrote:

Have you considered Stubba? It comes with the Mocha library.

http://mocha.rubyforge.org/

I’ve not come across Stubba before - it looks rather nice! Unfortunately
I’m not sure that it gives us what we’re after in this particular case
(please forgive me if I’m missing something). Maybe an example will
help.

What our current tests do is something along the following lines. Given
a class:

class Foo
def frobnicate
# Long and complicated code…
end

def gimbalize
# More complicated stuff…
end
end

We have tests which do something along the following lines:

module InstrumentedFoo
attr_reader was_frobnicated
def frobnicate
super
@was_frobnicated = true
end
end

module InterceptFooCreation
def new(options = {})
foo = super options
foo.extend InstrumentedFoo
return foo
end
end

def test_something
Foo.extend InterceptFooCreation
foo = … obtain a foo somehow …
assert foo.was_frobnicated
end

This all works, but is not exactly pleasant. What we would like is some
means by which we can ensure that every Foo object is instrumented
while running tests.

We can trivially achieve this using the test/mocks directory, as long as
we don’t mind completely replacing our Foo class, but it doesn’t seem to
help if we just want to extend the Foo class as above…

Suggestions gratefully received!

paul.butcher->msgCount++

Perhaps decorating your foo object and then creating a class method on
Foo to return the decorated object would work?

class InstrumentedFoo
attr_reader :was_frobnicated

def initialize(original_foo)
@foo = original_foo
end

def frobnicate
@foo.frobnicate
@was_frobnicated = true
end

proxy other methods to original foo

def method_missing(method, *args)
@foo.send(method, *args)
end
end

Foo.stubs(:instrumented_instance).returns(InstrumentedFoo.new(Foo.new))

example

@foo = Foo.instrumented_instance
@foo.gimbalize # still calls Foo#gimbalize
@foo.frobnicate # calls Foo#frobnicate
assert @foo.was_frombnicated

Or is that still not what you are after?

Paul B. wrote:

While running tests, we would like to instrument some of the classes
under test. We still want the classes to do exactly what they currently
do, but we would like them to do more when running in the test
environment.

Clearly we can (and currently do) just extend the objects “on the fly”
where necessary, but this is a bit messy - we’d like some centralised
way to always extend the objects for all tests. Is there any way of
using the test/mocks directory to achieve this? We can’t see any way to
use it to do anything other than completely replace a class.

Thanks in advance!

paul.butcher->msgCount++

Rails can do this easily, you just have to require_dependency on the
file in question and THEN reopen the class to change it.

#test/mocks/test/foo.rb
require_dependency “#{RAILS_ROOT}/lib/foo.rb”
class Foo
def bar

end
end

On 14/08/06, Paul B. [email protected] wrote:

While running tests, we would like to instrument some of the classes
under test. We still want the classes to do exactly what they currently
do, but we would like them to do more when running in the test
environment.

Clearly we can (and currently do) just extend the objects “on the fly”
where necessary, but this is a bit messy - we’d like some centralised
way to always extend the objects for all tests. Is there any way of
using the test/mocks directory to achieve this? We can’t see any way to
use it to do anything other than completely replace a class.

I’m not sure it’s exactly what you’re looking for, but you might find
the Stubba component of Mocha(http://mocha.rubyforge.org) useful. It
allows you to temporarily (for the duration of the test method)
replace method implementations with simple stubs which return a known
value.

James M…

On 14/08/06, Paul B. [email protected] wrote:

That would work fine if the test has control over the creation of the
object. But I’m not sure that it helps if its the code under test which
creates the object (which is the situation we’re in).

Can you give a simple example of what you’re trying to do? It’s much
easier discussing specifics.

Stubba allows you to stub any class method including “new” along the
following lines.

def test_me
Employee.stubs(:new).returns(anything_you_want)
company = Company.new
company.do_something_which_calls_new_on_employee
assert_something
end

James

On 14/08/06, James M. [email protected] wrote:

Can you give a simple example of what you’re trying to do? It’s much
easier discussing specifics.

Sorry - I somehow managed to miss your post which gave the example.

Hi Paul,

Are you able to expand upon your actual requirements? I’ve just been
speaking to James (mocha author) and we’re pretty certain we can help
but would like a little more clarification.

Cheers,

Chris

Luke R. wrote:

Perhaps decorating your foo object and then creating a class method on
Foo to return the decorated object would work?

Or is that still not what you are after?

That would work fine if the test has control over the creation of the
object. But I’m not sure that it helps if its the code under test which
creates the object (which is the situation we’re in).

Thanks!

paul.butcher->msgCount++

Chris R. wrote:

Are you able to expand upon your actual requirements? I’ve just been
speaking to James (mocha author) and we’re pretty certain we can help
but would like a little more clarification.

Hi Chris,

Apologies for the delayed reply - I use ruby-forum.com to keep up with
this list, and it seems to have been a bit “wobbly” recently :frowning:

Rather than concoct another example, I’ll give you a simplified version
of our actual code. Our system sends and receives (SMS) messages. We
have a Conversation model class which represents a set of incoming and
outgoing messages. One of the methods on Conversation is
“send_unsolicited” which creates a Conversation containing a single
outgoing message and then sends the message. The code goes something
like this:

class Conversation
class << self
def send_unsolicited(customer, message)
c = create_unsolicited(customer, message)
c.send_now!
c.unlock!
return c
end

def create_unsolicited(customer, message)
  c = Conversation.create :customer => customer, :locked_by_user_id 

=> 1
m = c.create_in_progress_outgoing_message :body => message
return c
end
end

def unlock!
self.locked_by_user = nil
save!
end

def create_in_progress_outgoing_message(options = {})
# …
end

def send_now!
# …
end
end

We have a test in which, among other things, we want to verify that
send_unsolicited unlocks the conversation. It’s not enough for us to
verify that the conversation isn’t locked when it returns - we need to
be certain that it’s actually called the unlock! method.

At the moment the test is as follows:

module HookCreate
def new(options = {})
c = super options
c.extend HookUnlock
return c
end
end

module HookUnlock
attr_reader :was_unlocked

def unlock!
super
@was_unlocked = true
end
end

def test_send_unsolicited
Conversation.extend HookCreate

c = Conversation.send_unsolicited customers(:customer1), “Blah blah
blah”
assert c.was_unlocked
end

Which works, but is nasty. And doesn’t scale well if more than one test
needs to do the same thing.

Make sense?

Ideally, we would like some way to say “in the test environment,
whenever you create a Conversation class, instrument it as follows”. The
test/mocks directory gives us a convenient way of saying “in the test
environment, whenever you create a Conversation class, create this mock
class instead”, but that’s not quite what we want :frowning:

If a mock could extend the class that it’s pretending to be instead of
simply replacing it, that would be perfect. But I’m not aware of any way
to achieve this in Rails as things stand.

Thanks in advance for your help!

paul.butcher->msgCount++

On 15/08/06, Paul B. [email protected] wrote:

def send_unsolicited(customer, message)
  return c

end

module HookUnlock

Ideally, we would like some way to say “in the test environment,
whenever you create a Conversation class, instrument it as follows”. The
test/mocks directory gives us a convenient way of saying “in the test
environment, whenever you create a Conversation class, create this mock
class instead”, but that’s not quite what we want :frowning:

If a mock could extend the class that it’s pretending to be instead of
simply replacing it, that would be perfect. But I’m not aware of any way
to achieve this in Rails as things stand.

Thanks in advance for your help!

Hi Paul,

You could use Mocha to do the following…

def test_send_unsolicited
conversation = mock()
conversation.stubs(:send_now!)
conversation.expects(:unlock!)
Conversation.stubs(:create_unsolicited).returns(conversation)
Conversation.send_unsolicited(nil, nil)
end

Mocha will auto-verify the expectation that Conversation#unlock! is
called at the end of the test_send_unsolicited method (in a secret
teardown). You no longer need any of the Hook modules. Note that all
stubbed/expected methods will not execute their normal
implementations, however the class will be put back to normal after
the test_send_unsolicited method ends. This test is stubbing out a lot
of code, but it is focussed on verifying the unlock! call. An
alternative is…

def test_send_unsolicited
Conversation.any_instance.expects(:unlock!)
Conversation.send_unsolicited(customers(:customer1), “Blah blah blah”)
Conversation.any_instance.verify
end

This only replaces the unlock! method and verifies that it was called,
so in this case you need to supply the parameters to send_unsolicited,
so that create_unsolicited has something to work with. In this case,
with the current version of Mocha, you need to manually call verify.

I’m afraid I haven’t had time to check this works, but I think it should
be ok.

Make any sense?

James.

On 16/08/06, Paul B. [email protected] wrote:

Perfect sense. I’m installing Mocha now… :slight_smile:

Great! I’d welcome any feedback.

James.

James M. wrote:

You could use Mocha to do the following…

def test_send_unsolicited
conversation = mock()
conversation.stubs(:send_now!)
conversation.expects(:unlock!)
Conversation.stubs(:create_unsolicited).returns(conversation)
Conversation.send_unsolicited(nil, nil)
end

… This test is stubbing out a lot
of code, but it is focussed on verifying the unlock! call.

Which means that we can’t use this approach in our particular case -
although the only thing that the example I gave you does is verify that
unlock! is called, the “real” test does quite a bit more :slight_smile:

However:

An alternative is…

def test_send_unsolicited
Conversation.any_instance.expects(:unlock!)
Conversation.send_unsolicited(customers(:customer1), “Blah blah blah”)
Conversation.any_instance.verify
end

This only replaces the unlock! method and verifies that it was called,
so in this case you need to supply the parameters to send_unsolicited,
so that create_unsolicited has something to work with. In this case,
with the current version of Mocha, you need to manually call verify.

I think that this gives us exactly what we want :slight_smile:

Thank you!

Make any sense?

Perfect sense. I’m installing Mocha now… :slight_smile:

Cheers!

paul.butcher->msgCount++