3

I use a decorator module that get's included in a model instance (through the "extends" method). So for example :

module Decorator
  def foo
  end
end

class Model < ActiveRecord::Base
end

class ModelsController < ApplicationController
  def bar
    @model = Model.find(params[:id])
    @model.extend(Decorator)
    @model.foo
  end
end

Then I would like in the tests to do the following (using Mocha) :

test "bar" do
  Model.any_instance.expects(:foo).returns("bar")
  get :bar
end 

Is this possible somehow, or do you have in mind any other way to get this functionality???

Vega
  • 27,856
  • 27
  • 95
  • 103
Nikos D
  • 431
  • 4
  • 14

4 Answers4

2

Just an Assumption Note: I will assume that your Decorator foo method returns "bar" which is not shown in the code that you sent. If I do not assume this, then expectations will fail anyway because the method returns nil and not "bar".

Assuming as above, I have tried the whole story as you have it with a bare brand new rails application and I have realized that this cannot be done. This is because the method 'foo' is not attached to class Model when the expects method is called in your test.

I came to this conclusion trying to follow the stack of called methods while in expects. expects calls stubs in Mocha::Central, which calls stubs in Mocha::ClassMethod, which calls *hide_original_method* in Mocha::AnyInstanceMethod. There, *hide_original_method* does not find any method to hide and does nothing. Then Model.foo method is not aliased to the stubbed mocha method, that should be called to implement your mocha expectation, but the actual Model.foo method is called, the one that you dynamically attach to your Model instance inside your controller.

My answer is that it is not possible to do it.

p.matsinopoulos
  • 7,655
  • 6
  • 44
  • 92
  • This is helpful and actually this is the source of the problem. I would like to ask the Mocha developers if there is a way to actually do this. From a relevant discussion they have explicitly said though that mocking Module methods is not possible. Only Class ones... :( – Nikos D Oct 10 '11 at 20:59
1

It works (confirmed in a test application with render :text)

I usually include decorators (instead of extending them at runtime) and I avoid any_instance since it's considered bad practice (I mock find instead).

module Decorators
  module Test
    def foo
      "foo"
    end
  end
end

class MoufesController < ApplicationController

  def bar
    @moufa = Moufa.first
    @moufa.extend(Decorators::Test)
    render :text => @moufa.foo
  end
end

require 'test_helper'

class MoufesControllerTest < ActionController::TestCase
  # Replace this with your real tests.
  test "bar" do
    m = Moufa.first
    Moufa.expects(:find).returns(m)
    m.expects(:foo).returns("foobar")

    get :bar, {:id => 32}
    assert_equal @response.body, "foobar"
  end
end
bandito
  • 447
  • 2
  • 2
  • You may be right about the include vs extend but in some cases you may not want to extend all instances of a class but only a specific one. Which mocha version are you using and it works? It fails on my machine (Mocha error) – Nikos D Oct 10 '11 at 20:57
  • Well, reading the answers above, I think the problem is any_instance (I don't use any_instance at all, it doesn't even exist in RSpec) – bandito Oct 11 '11 at 05:42
  • Yeap, stubbing the find makes sense. It's one level more in terms of mocking/stubbing which is not a good thing but it works and it's better than my workaround. Thanks. – Nikos D Oct 11 '11 at 07:00
1

Ok, now I understand. You want to stub out a call to an external service. Interesting that mocha doesn't work with extend this way. Besides what is mentioned above, it seems to be because the stubbed methods are defined on the singleton class, not the module, so don't get mixed in.

Why not something like this?

test "bar" do
  Decorator = Module.new{ def foo; 'foo'; end }
  get :bar
end

If you'd rather not get the warnings about Decorator already being defined -- which is a hint that there's some coupling going on anyway -- you can inject it:

class ModelsController < ApplicationController
  class << self
    attr_writer :decorator_class
    def decorator_class; @decorator_class ||= Decorator; end
  end

  def bar
    @model = Model.find(params[:id])
    @model.extend(self.class.decorator_class)
    @model.foo
  end
end

which makes the test like:

test "bar" do
  dummy = Module.new{ def foo; 'foo'; end }
  ModelsController.decorator_class = dummy
  get :bar
end

Of course, if you have a more complex situation, with multiple decorators, or decorators defining multiple methods, this may not work for you.

But I think it is better than stubbing the find. You generally don't want to stub your models in an integration test.

Eric G
  • 1,282
  • 8
  • 18
  • Thanks Eric for the reply. This actually what I'm already doing (almost). Basically this and bandito's reply are the two options that actually work. – Nikos D Oct 12 '11 at 07:22
0

One minor change if you want to test the return value of :bar -

test "bar" do
  Model.any_instance.expects(:foo).returns("bar")
  assert_equal "bar", get(:bar)
end

But if you are just testing that a model instance has the decorator method(s), do you really need to test for that? It seems like you are testing Object#extend in that case.

If you want to test the behavior of @model.foo, you don't need to do that in an integration test - that's the advantage of the decorator, you can then test it in isolation like

x = Object.new.extend(Decorator)
#.... assert something about x.foo ...

Mocking in integration tests is usually a code smell, in my experience.

Eric G
  • 1,282
  • 8
  • 18
  • 1
    No, I don't want to test the return value of the controller action (and thus the method). I just want to mock the extended method and thus to "cut" the real action (which is an external call) for the test object. Mocha fails recording the interaction with this object. – Nikos D Oct 10 '11 at 20:56