[mocha-developer] Rails functional testing and Mocha
Frederick Cheung
fred at 82ask.com
Sun Mar 4 06:41:35 EST 2007
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
More information about the mocha-developer
mailing list