Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

instance_double not matching the same class it is mocking #794

Closed
plukevdh opened this issue Oct 1, 2014 · 18 comments
Closed

instance_double not matching the same class it is mocking #794

plukevdh opened this issue Oct 1, 2014 · 18 comments

Comments

@plukevdh
Copy link

plukevdh commented Oct 1, 2014

I have a test case wherin an instance_double of a class does not think it is a kind_of/is_a/=== of the mocked class.

https://gist.github.com/plukevdh/7438f96ec0bd4dd65de8

May be related to issue filed/closed #749. Am I misunderstanding how this ought to work?

Currently using RSpec 3.1.5

@plukevdh plukevdh changed the title instance_double type matching not equal to mocked class. instance_double not matching the same class it is mocking Oct 1, 2014
@myronmarston
Copy link
Member

This is by design. instance_double is meant simply to constraint your mocking and stubbing by the interface of the named class, but isn't meant to be an instance of the named class. They are meant to fit the same "role" as the named class, and provide the same interface, but not be the same class. You can use a partial double (e.g. a real object with a couple methods mocked or stubbed) to get something that is the same class.

@myronmarston
Copy link
Member

To expand on this a bit more...besides the fact that instance_double isn't designed to work that way, there are also implementation difficulties: to make your spec pass, instance_double(MyClass) would have to stub MyClass#===, which would be a hidden, surprising stub. We don't want to do that.

@dmolesUC
Copy link

I don't think it's true that you can work around this with a partial double. Given the following:

class Foo
end

class Bar
end

class Baz
  def frob(x)
    case x
    when Foo
      'foo'
    when Bar
      'bar'
    else
      raise "expected Foo or Bar, got: #{x}"
    end
  end
end

require 'rspec'

describe Baz do
  describe '#frob' do
    it 'frobs a Foo' do
      foo = double(Foo.new)
      baz = Baz.new
      expect(baz.frob(foo)).to eq('foo')
    end
  end
end

I get:

Failures:

  1) Baz#frob frobs a Foo
     Failure/Error: raise "expected Foo or Bar, got: #{x}"
     RuntimeError:
       expected Foo or Bar, got: #[RSpec::Mocks::Double:0x3ff659d82db0 @name=#[Foo:0x007fecb3b05c78]]

In terms of test simplicity, if you don't need your 'mock' to do anything but you want to avoid some complex initialization, you can use Foo.allocate to create an 'empty' object of the correct class.

@myronmarston
Copy link
Member

I don't think it's true that you can work around this with a partial double. Given the following:

You're example isn't using a partial double. (At least, it's not using what we mean by "partial double"). double(Foo.new) is not a partial double. It's a test double with Foo.new provided as the name.

Foo.new can be a partial double, and will be, as soon as you mock or stub a method on it.

In terms of test simplicity, if you don't need your 'mock' to do anything but you want to avoid some complex initialization, you can use Foo.allocate to create an 'empty' object of the correct class.

True, although Foo.allocate returns an object that has all the original implementations. In fact, it's identical to what is returned by Foo.new except for the fact that initialize has been skipped and it has no instance variables initialized.

@dmolesUC
Copy link

You're right, I totally failed to understand the partial double documentation. Sorry, coming out of Mockito where you have to wrap any real object before you can stub its methods. (The fact that the return values in the partial double docs are also doubles is confusing to newcomers, or at least this newcomer.)

If I wanted to stub MyClass#===, on purpose, out in the open with no surprises, how would I do it? There doesn't seem to be an obvious way to get the doubled class of an InstanceDouble.

@myronmarston
Copy link
Member

If I wanted to stub MyClass#===, on purpose, out in the open with no surprises, how would I do it? There doesn't seem to be an obvious way to get the doubled class of an InstanceDouble.

You're right. Problem is, it doesn't really make sense to provide double.doubled_class on the verified double since the class it's doubling doesn't provide that interface. We may need to add a new API for this, although I'm not sure what it would be...

In the meantime, you could do this:

module VerifiedDoubleExtensions
  def instance_double(klass, *args)
    super.tap do |dbl|
      allow(klass).to receive(:===).and_call_original # do the normal thing by default
      allow(klass).to receive(:===).with(dbl).and_return true
    end
  end
end

RSpec.configure do |c|
  c.include VerifiedDoubleExtensions
end

This would make instance_double(MyClass) both create the instance double, and stub MyClass.=== to return true when passed the instance double.

@dmolesUC
Copy link

Clever! Thanks.

@fornellas
Copy link

@myronmarston , I understand an instance_double's design idea you explained, but if you look at Module#=== implementation, it actually calls the operand's kind_of? method. So, we can get a "full" behavior by:

let(:test) { instance_double(Integer) }
allow(test)
  .to receive(:kind_of?).with(Integer)
  .and_return(true)

Of course, other methods would have to be mocked also (#class, #is_a? etc). This could be another design approach ("instance_double_on_steroids"?!). What do you think?

@myronmarston
Copy link
Member

@fornellas that sound like a good idea for an extension gem. If/when it sees significant usage by the community, we might consider rolling it in to RSpec in a future release.

@fornellas
Copy link

D'oh! Although Module#=== calls rb_obj_is_kind_of(), it in turn, does not seem to call the mock on the instance double. This approach does not work. One (hard) option would be to use a real object, created with BasicObject.allocate, and then, overwrite all existing methods on the class hierarchy, plus method_mising, to mimic instance_double. Even then, if the class hierarchy changes, things would break. We'd have to use hooks to protect against it.
In sum: hard to implement, easy to break. Not worth it.

@dmolesUC
Copy link

In practice I've mostly been able to work around it using MyClass.allocate instead of instance_double(MyClass).

@fornellas
Copy link

@dmolesUC3 , that would be appropriate. The disadvantage over instance doubles, is that an allocated instance won't raise upon a not mocked method, and since it does not call #initialize, you'd surely get some odd behaviors...

@chronodm
Copy link

chronodm commented May 2, 2016

It does depend on exactly what you're testing, certainly. With luck (and attention to detail) you won't have too many cases where the "double" needs to pass is_a?, and you can use instance_double for everything else.

@Startouf
Copy link

Startouf commented Nov 2, 2018

It seems that stubbing those calls to kind_of / is_a no longer works (this is now pure C code when I look at https://ruby-doc.org/core-2.2.0/Module.html#method-i-3D-3D-3D)

(Ruby 2.2+)

@Startouf
Copy link

Ah, actually there is a way to do it directly via === instead. Credits to https://stackoverflow.com/a/55928312/2832282

it 'make some business logic here' do
  allow(Foo).to receive(:===).with(foo).and_return(true)
  expect { subject }.to be_truthy
end

@thefotios
Copy link

thefotios commented Jul 31, 2020

Just came across this and thought I'd add my solution (which borrows from a few posted here) for anybody else that comes across this.

Delegating to klass.allocate should work 99% of the time unless you're doing some sort of multi-dispatch/factory in #new or overload any of the CLASS_EQUIVALENCE_FUNCTIONS to use object specific attributes.

# Updated based on https://github.com/rspec/rspec-mocks/issues/794#issuecomment-667199482
module VerifiedDoubleExtensions
  CLASS_EQUIVALENCE_FUNCTIONS = %i[is_a? kind_of? instance_of?].freeze

  def verified_double(klass, *args)
    instance_double(klass, *args).tap do |dbl|
      CLASS_EQUIVALENCE_FUNCTIONS.each do |fn|
        allow(dbl).to receive(fn) do |*fn_args|
          klass.allocate.send(fn, *fn_args)
        end
      end
      allow(klass).to receive(:===).and_call_original # do the normal thing by default
      allow(klass).to receive(:===).with(dbl).and_return true
    end
  end
end

See https://gist.github.com/thefotios/23912e524f585d855606dbec713df388 for history

@JonRowe
Copy link
Member

JonRowe commented Jul 31, 2020

klass.allocate this is a bad idea FWIW, if the object relies on initialize to setup data your invocations will error, you'd be better off using the kernel defined methods.

@thefotios
Copy link

@JonRowe agreed. That sort of data should be irrelevant to checking for is_a? and kind_of? (I can't really think of a good reason somebody would explicitly override those, but I'm sure there are some edge cases, hence the "should work 99% of the time" disclaimer).

=== is a different beast. I'm going to steal from #794 (comment) and update how that's handled.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants