1
votes

In rspec 3.1.0, a spec can use #allow to stub a method:

describe do
  specify do
    o = Object.new
    allow(o).to receive(:foo)
    o.foo
  end
end

This works fine. However, if the stub is within a method in a class, then the #allow method is not defined:

describe do

  class Baz
    def allow_bar(o)
      allow(o).to receive(:bar)
    end
  end

  specify do
    o = Object.new
    Baz.new.allow_bar(o)
    o.bar
  end

end

The error is:

 Failure/Error: allow(o).to receive(:bar)
 NoMethodError:
   undefined method `allow' for #<Baz:0x8de6720>
 # ./bar_spec.rb:5:in `allow_bar'
 # ./bar_spec.rb:11:in `block (2 levels) in <top (required)>'

Why I am stubbing within a class

The test defines its test double as a regular class rather than using rspec's "double" method. This is because the test double has a thread. Inside the test double is this code:

if command == :close
  # Note: Uses the old (rspec 2) syntax.  Needs to be converted
  # to rspec 3 syntax using the #allow method.
  socket.stub(:getpeername).and_raise(RuntimeError, "Socket closed")
end

This prevents the code under test from erroneously using the socket after the session has been closed.

A private solution

I can give the test double access to #allow by calling a private API in rspec-mock:

  class Baz
    RSpec::Mocks::Syntax.enable_expect self    # Uses private API
    def allow_bar(o)
      allow(o).to receive(:bar)
    end
  end

This works. However, it is explicitly marked as a private API in rspec/mocks/syntax.rb:

  # @api private
  # Enables the expect syntax (`expect(dbl).to receive`, `allow(dbl).to receive`, etc).
  def self.enable_expect(syntax_host = ::RSpec::Mocks::ExampleMethods)
  ...

The Question

In Rspec 3.1, is there a public API I can use to make the expect syntax available within a class?

1
There should be a way to inject the socket into your class. Then, you will no longer need to use allow inside a class. AFAIK RSpec 3 tries to remove as much monkey patching as possible, which is a good thing. It sounds like a lazy answer but when your code is hard to test, most of the time it's a sign that your code has some other problems.Patrick Oscity
@p11y That's usually true. I might post the full code on CR and see if someone else can see the better design that I don't see.Wayne Conrad
@p11y I started to write the CR question, then realized a better way (in this case, making two private methods public did the trick, with no real damage to the class's API). Yet another question answered in the asking. Thanks for your help. I am still interested in the answer to this question, for the next time I want to do it wrong.Wayne Conrad

1 Answers

0
votes

You can accomplish this by mixing in a few RSpec modules/classes into your class:

class MyClass
  include RSpec::Mocks::ExampleMethods::ExpectHost
  include RSpec::Matchers 

  def some_test
    expect(1).to eql(1)
  end
end