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