btakita / rr

RR (Double Ruby) is a test double framework that features a rich selection of double techniques and a terse syntax.
http://github.com/rr/rr
MIT License
501 stars 58 forks source link

How do I mock a module method that is mixed in at runtime in a ruby object ? #98

Closed archit closed 11 years ago

archit commented 11 years ago

I have ruby code similar to this

module Validator
  def invalid?
    make_http_request
  end
end

class Subject
  def foo(obj)
    if obj.extend(Validator).invalid?
      do_something
    else
      do_something_else
    end
  end
end

In my unit test for Subject#foo method, I wanted to stub out the invalid? call. My test looks like this

class SubjectTest < UnitTest
  setup do
    @object = Resource.new
    @subject = Subject.new
  end
  should "do_something_else if resource is valid" do
    stub(@object).invalid? { false }
    @subject.foo(@object)
  end
end

However my stub's block never gets called. I imagine its because the stub definite gets 'installed', but since the mixin is done at runtime, the Validator#invalid? overwrites my stub. The other option I tried was to stub the method on the module

stub(Validator).invalid? { false }

but I have the same issue, where my stub block isn't being used/called.

Anyone have any ideas, thoughts or suggestions ?

mcmire commented 11 years ago

The way that RR works is that if you are stubbing (or mocking) a method that doesn't exist, instead of creating a method within that object which calls your block, it actually creates a #method_missing method which calls your block (assuming the missing method is the one you're stubbing/mocking). Before obj.extend(Validator), #invalid? on object doesn't exist, and so when you say stub(@object).invalid? { false }, RR will create #method_missing. Once the object is extended with Validator, then #invalid? springs into existence, and that essentially ends up overriding #method_missing.

Honestly I'm not sure what the reasoning behind this was so I'd have to look into this for you. But at least at the moment you can try something like this:

class SubjectTest < UnitTest
  setup do
    @object = Resource.new
    @subject = Subject.new
  end

  should "do_something_else if resource is valid" do
    @object.extend(Module.new { def invalid?; end })
    stub(@object).invalid? { false }
    @subject.foo(@object)
  end
end

Here we mix an anonymous module into @object which defines a dummy #invalid? method. This way, when the stub occurs, it will replace #invalid? rather than defining #method_missing. When @subject.foo is called, Validator will be mixed into @object which will override our dummy method but not override the stub method (since that is defined directly in @object's metaclass).

archit commented 11 years ago

@mcmire ah good to know about how the internals work. Yeah I'm primarily what I'm looking to do is really stub out a whole module, so I'm going with my monkey-patching way. I'll try out your way in the meantime. Thanks!

mcmire commented 11 years ago

Okay, cool. I'm closing this issue since it's a question, but let me know if it doesn't work and I'll try to help you further.