Paul Butcher
2006-Aug-14 11:19 UTC
[Rails] 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++ -- Posted via http://www.ruby-forum.com/.
James Mead
2006-Aug-14 12:05 UTC
[Rails] A mock which extends rather than replaces a class?
On 14/08/06, Paul Butcher <paul@paulbutcher.com> 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 Mead. http://blog.floehopper.org
Luke Redpath
2006-Aug-14 13:07 UTC
[Rails] Re: A mock which extends rather than replaces a class?
Paul Butcher 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/ -- Posted via http://www.ruby-forum.com/.
Paul Butcher
2006-Aug-14 13:47 UTC
[Rails] Re: A mock which extends rather than replaces a class?
Luke Redpath 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++ -- Posted via http://www.ruby-forum.com/.
Luke Redpath
2006-Aug-14 14:08 UTC
[Rails] Re: A mock which extends rather than replaces a class?
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? -- Posted via http://www.ruby-forum.com/.
Paul Butcher
2006-Aug-14 14:25 UTC
[Rails] Re: A mock which extends rather than replaces a class?
Luke Redpath wrote:> Perhaps decorating your foo object and then creating a class method on > Foo to return the decorated object would work? > > <snip> > > 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++ -- Posted via http://www.ruby-forum.com/.
James Mead
2006-Aug-14 15:48 UTC
[Rails] Re: A mock which extends rather than replaces a class?
On 14/08/06, Paul Butcher <paul@paulbutcher.com> 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 http://blog.floehopper.org
Alex Wayne
2006-Aug-14 21:46 UTC
[Rails] Re: A mock which extends rather than replaces a class?
Paul Butcher 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 -- Posted via http://www.ruby-forum.com/.
James Mead
2006-Aug-15 08:28 UTC
[Rails] Re: A mock which extends rather than replaces a class?
On 14/08/06, James Mead <jamesmead44@gmail.com> 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.
Chris Roos
2006-Aug-15 11:21 UTC
[Rails] Re: A mock which extends rather than replaces a class?
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 On 8/15/06, James Mead <jamesmead44@gmail.com> wrote:> On 14/08/06, James Mead <jamesmead44@gmail.com> 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. > _______________________________________________ > Rails mailing list > Rails@lists.rubyonrails.org > http://lists.rubyonrails.org/mailman/listinfo/rails >
Paul Butcher
2006-Aug-15 15:43 UTC
[Rails] Re: Re: A mock which extends rather than replaces a class?
Chris Roos 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 :-( 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 :-( 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++ -- Posted via http://www.ruby-forum.com/.
James Mead
2006-Aug-15 16:31 UTC
[Rails] Re: Re: A mock which extends rather than replaces a class?
On 15/08/06, Paul Butcher <paul@paulbutcher.com> wrote:> 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 :-( > > 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. http://blog.floehopper.org
Paul Butcher
2006-Aug-16 11:55 UTC
[Rails] Re: Re: Re: A mock which extends rather than replaces a clas
James Mead 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 :-) 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 :-) Thank you!> Make any sense?Perfect sense. I''m installing Mocha now... :-) Cheers! paul.butcher->msgCount++ -- Posted via http://www.ruby-forum.com/.
James Mead
2006-Aug-16 12:01 UTC
[Rails] Re: Re: Re: A mock which extends rather than replaces a clas
On 16/08/06, Paul Butcher <paul@paulbutcher.com> wrote:> > Perfect sense. I''m installing Mocha now... :-)Great! I''d welcome any feedback. James. http://blog.floehopper.org -------------- next part -------------- An HTML attachment was scrubbed... URL: http://wrath.rubyonrails.org/pipermail/rails/attachments/20060816/b3a2262c/attachment.html
James Mead
2006-Aug-16 12:02 UTC
[mocha-developer] Fwd: [Rails] Re: Re: Re: A mock which extends rather than replaces a clas
---------- Forwarded message ---------- From: Paul Butcher <paul at paulbutcher.com> Date: 16-Aug-2006 12:55 Subject: [Rails] Re: Re: Re: A mock which extends rather than replaces a clas To: rails at lists.rubyonrails.org James Mead 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 :-) 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 :-) Thank you!> Make any sense?Perfect sense. I''m installing Mocha now... :-) Cheers! paul.butcher->msgCount++ -- Posted via http://www.ruby-forum.com/. _______________________________________________ Rails mailing list Rails at lists.rubyonrails.org http://lists.rubyonrails.org/mailman/listinfo/rails -------------- next part -------------- An HTML attachment was scrubbed... URL: http://rubyforge.org/pipermail/mocha-developer/attachments/20060816/51e178b3/attachment.html