Mocking/stubbing a method that's included from "instance.extend(DecoratorModule)"

2.9k views Asked by At

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

4

There are 4 answers

3
bandito On BEST ANSWER

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
1
Eric G On

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.

1
Eric G On

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.

1
p.matsinopoulos On

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.