[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