[mocha-developer] Rails functional testing and Mocha

David Chelimsky dchelimsky at gmail.com
Sun Mar 4 07:47:35 EST 2007


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.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
>


More information about the mocha-developer mailing list