Frederick Cheung
2007-Mar-04 11:41 UTC
[mocha-developer] Rails functional testing and Mocha
I''ve always wanted to be able to do stuff like this in my functional tests c = customers(:customer_1) c.expects(:great_customer_service) post :service_customer, :id => c.id This of course fails because inside the rails action a different instance of customer is used. Some of the time setting your expectation/stubbing on Customer.any_instance works, but it''s not beautiful and of course breaks down if there are multiple customers installed. So I mucked around a bit and came of with the following. It''s very ActiveRecord specific, but that''s what I''ve been dealing with.. I have a model Customer, and concrete subclass EightTwoAskCustomer class EightTwoAskCustomer < Customer def bill #take all their money end end A controller: class TestController < ApplicationController def bill c = Customer.find params[:id] c.bill render :nothing => true end end A functional test: [snip boiler plate] require ''eight_two_ask_customer'' class EightTwoAskCustomer include Mocha::ExpectationLoader end [snip more boiler plate] # Replace this with your real tests. def test_bill_customer c = customers(:customer1) EightTwoAskCustomer.any_instance_with_id( c.id).expects(:bill) post :bill, :id => c.id end This does mostly what I want, the test passes. If I comment out c.bill I get a failure: #<Mock:0x11e54b6>.bill - expected calls: 1, actual calls: 0 If I try to increase gross margins by billing the customer twice I get a failure too: #<Mock:0x11e5312>.bill - expected calls: 1, actual calls: 2 Implementation wise it looks like this: A intermediary class AnyInstanceWithID. A method is defined on Class that provides you with instances of it def any_instance_with_id the_id @AnyInstances = {} unless defined? @AnyInstances @AnyInstances[the_id] ||= AnyInstanceWithID.new() end AnyInstanceWithID is a sort of proxy thing on which you set expectations. It keeps them until it can set them on the real things. It looks very much like Mocha::Mock (there''s some refactoring to be done here), except that it doesn''t actually do anything in terms of undefining methods. class AnyInstanceWithID attr_reader :expectations def initialize @expectations = [] end def stubs(method_names) method_names = method_names.is_a?(Hash) ? method_names : { method_names => nil } method_names.each do |method_name, return_value| expectations << Mocha::Stub.new(nil, method_name, caller).returns(return_value) end expectations.last end def expects(method_names) method_names = method_names.is_a?(Hash) ? method_names : { method_names => nil } method_names.each do |method_name, return_value| expectations << Mocha::Expectation.new(nil, method_name, caller).returns(return_value) end expectations.last end end Lastly we define an implementation of after_find that causes the expectations to be set as the objects are loaded module Mocha module ExpectationLoader def after_find if any = self.class.any_instance_with_id( self.id) any.expectations.each do |e| method = stubba_method.new(stubba_object, e.method_name) $stubba.stub(method) e.mock = self.mocha self.mocha.expectations << e self.mocha.__metaclass__.send(:undef_method, e.method_name) if self.mocha.__metaclass__.method_defined? (e.method_name) end end end end end There''s also one or 2 places where i''ve made attributes writable/ readable on Expectation etc... to get this all to hold together. Limitations: - The big limitation is that if an instance with the required id is not loaded than the test does not fail (since the expectation was never set). There may well be other nasties lurking. - The syntax is also rather awkward, but I''m sure someone will think of a nice way to name it all - I should be a good boy and check if after_find is already defined and call back into the existing one - Behaviour w.r.t subclasses is a little confusing: I have to set my expectation on EightTwoAskCustomer.any_instance_with_id, setting it on the parent class Customer doesn''t work - would be nice if using any_instance_with_id caused it to include ExpectationLoader for you. So, first of all: am I completely off my rocker ? Please tell me if I''m wasting my time/ missing the point/ missing a more obvious way of accomplishing this - Any ideas for tidying up some of the limitations ? Anyway, sorry for the long email, I look forward to hearing any comments. Fred
David Chelimsky
2007-Mar-04 12:47 UTC
[mocha-developer] Rails functional testing and Mocha
On 3/4/07, Frederick Cheung <fred at 82ask.com> wrote:> I''ve always wanted to be able to do stuff like this in my functional > tests > > c = customers(:customer_1) > c.expects(:great_customer_service) > post :service_customer, :id => c.idFWIW, I like to handle this sort of thing like this: mock_customer = mock mock_customer.expects(:great_customer_service) Customer.expects(:find).with("37").returns(mock_customer) post :service_customer, :id => "37" This works if your viewpoint is that rails functionals are really "controller and view" tests and that they shouldn''t depend on real models (which is my viewpoint). Even if you think real models should be in your functionals, you can do it this way: customer = customers(:customer_1) customer.expects(:great_customer_service) Customer.expects(:find).with(customer.id.to_s).returns(c) post :service_customer, :id => customer.id This is not to suggest that you''re approach is wrong or that the one I propose is "right". It just aligns better w/ my personal views. And it allows you to do what you want without monkey patching mocha. Cheers, David> > This of course fails because inside the rails action a different > instance of customer is used. Some of the time setting your > expectation/stubbing on Customer.any_instance works, but it''s not > beautiful and of course breaks down if there are multiple customers > installed. > > So I mucked around a bit and came of with the following. It''s very > ActiveRecord specific, but that''s what I''ve been dealing with.. > I have a model Customer, and concrete subclass EightTwoAskCustomer > > class EightTwoAskCustomer < Customer > def bill > #take all their money > end > end > > A controller: > > class TestController < ApplicationController > def bill > c = Customer.find params[:id] > c.bill > render :nothing => true > end > end > > A functional test: > [snip boiler plate] > > require ''eight_two_ask_customer'' > class EightTwoAskCustomer > include Mocha::ExpectationLoader > end > > [snip more boiler plate] > # Replace this with your real tests. > def test_bill_customer > c = customers(:customer1) > EightTwoAskCustomer.any_instance_with_id( c.id).expects(:bill) > post :bill, :id => c.id > end > > This does mostly what I want, the test passes. If I comment out > c.bill I get a failure: > #<Mock:0x11e54b6>.bill - expected calls: 1, actual calls: 0 > If I try to increase gross margins by billing the customer twice I > get a failure too: > #<Mock:0x11e5312>.bill - expected calls: 1, actual calls: 2 > > Implementation wise it looks like this: > > A intermediary class AnyInstanceWithID. A method is defined on Class > that provides you with instances of it > > def any_instance_with_id the_id > @AnyInstances = {} unless defined? @AnyInstances > @AnyInstances[the_id] ||= AnyInstanceWithID.new() > end > > AnyInstanceWithID is a sort of proxy thing on which you set > expectations. It keeps them until it can set them on the real > things. It looks very much like Mocha::Mock (there''s some > refactoring to be done here), except that it doesn''t actually do > anything in terms of undefining methods. > > class AnyInstanceWithID > attr_reader :expectations > def initialize > @expectations = [] > end > > def stubs(method_names) > method_names = method_names.is_a?(Hash) ? method_names : > { method_names => nil } > method_names.each do |method_name, return_value| > expectations << Mocha::Stub.new(nil, method_name, > caller).returns(return_value) > end > expectations.last > end > > def expects(method_names) > method_names = method_names.is_a?(Hash) ? method_names : > { method_names => nil } > method_names.each do |method_name, return_value| > expectations << Mocha::Expectation.new(nil, method_name, > caller).returns(return_value) > end > expectations.last > end > end > > Lastly we define an implementation of after_find that causes the > expectations to be set as the objects are loaded > > module Mocha > module ExpectationLoader > def after_find > if any = self.class.any_instance_with_id( self.id) > any.expectations.each do |e| > method = stubba_method.new(stubba_object, e.method_name) > $stubba.stub(method) > e.mock = self.mocha > self.mocha.expectations << e > self.mocha.__metaclass__.send(:undef_method, > e.method_name) if self.mocha.__metaclass__.method_defined? > (e.method_name) > end > end > end > end > end > > There''s also one or 2 places where i''ve made attributes writable/ > readable on Expectation etc... to get this all to hold together. > > Limitations: > - The big limitation is that if an instance with the required id is > not loaded than the test does not fail (since the expectation was > never set). There may well be other nasties lurking. > - The syntax is also rather awkward, but I''m sure someone will think > of a nice way to name it all > - I should be a good boy and check if after_find is already defined > and call back into the existing one > - Behaviour w.r.t subclasses is a little confusing: I have to set my > expectation on EightTwoAskCustomer.any_instance_with_id, setting it > on the parent class Customer doesn''t work > - would be nice if using any_instance_with_id caused it to include > ExpectationLoader for you. > > > So, first of all: am I completely off my rocker ? Please tell me if > I''m wasting my time/ missing the point/ missing a more obvious way of > accomplishing this > - Any ideas for tidying up some of the limitations ? > > Anyway, sorry for the long email, I look forward to hearing any > comments. > > Fred > _______________________________________________ > mocha-developer mailing list > mocha-developer at rubyforge.org > http://rubyforge.org/mailman/listinfo/mocha-developer >
Frederick Cheung
2007-Mar-04 13:16 UTC
[mocha-developer] Rails functional testing and Mocha
On 4 Mar 2007, at 12:47, David Chelimsky wrote:> On 3/4/07, Frederick Cheung <fred at 82ask.com> wrote: >> I''ve always wanted to be able to do stuff like this in my functional >> tests >> >> c = customers(:customer_1) >> c.expects(:great_customer_service) >> post :service_customer, :id => c.id > > FWIW, I like to handle this sort of thing like this: > > mock_customer = mock > mock_customer.expects(:great_customer_service) > Customer.expects(:find).with("37").returns(mock_customer) > post :service_customer, :id => "37" > > This works if your viewpoint is that rails functionals are really > "controller and view" tests and that they shouldn''t depend on real > models (which is my viewpoint). Even if you think real models should > be in your functionals, you can do it this way: > > customer = customers(:customer_1) > customer.expects(:great_customer_service) > Customer.expects(:find).with(customer.id.to_s).returns(c) > post :service_customer, :id => customer.idThat''s neat, definitely a nice approach when it fits in (and I can certainly see why you would have the viewpoint you do). Thanks, Fred
Hi Fred, It''s great to see you playing around with the internals of Mocha. I hope you''ve been having fun! I use the same technique that David describes (although I''d probably stub the call to find not expect it, because its a query not a command). In fact I rarely use the any_instance form of stubbing. It feels a bit dirty! I don''t think we''d want to incorporate the change you''re suggesting, because (as you say) its a bit ActiveRecord-specific and there is an alternative. Is there anything you don''t like about the technique David suggests (i.e. stubbing the find method)? -- James. http://blog.floehopper.org
Frederick Cheung
2007-Mar-05 11:48 UTC
[mocha-developer] Rails functional testing and Mocha
On 5 Mar 2007, at 11:15, James Mead wrote:> Hi Fred, > > It''s great to see you playing around with the internals of Mocha. I > hope > you''ve been having fun! > > I use the same technique that David describes (although I''d > probably stub > the call to find not expect it, because its a query not a command). > In fact I rarely use the any_instance form of stubbing. It feels a bit > dirty! > > I don''t think we''d want to incorporate the change you''re > suggesting, because > (as you say) its a bit ActiveRecord-specific and there is an > alternative. > > Is there anything you don''t like about the technique David suggests > (i.e. > stubbing the find method)? >I like it very much actually. In general though I am a little nervous about testing everything in isolation i.e. models in their unit tests controllers & views in their functional etc... I like knowing that I''ve taken a complete path through the system. Perhaps some of my functional tests should actually be integration tests but that doesn''t really make a distance here. I can also see it getting a little hairy if you''re faking up the finding of multiple objects, with associations etc... Fred