0

Code being tested:

class Session
  def initialize
    @interface = Interface.new(self)
    @interface.hello
  end
end

class Interface
  def initialize(session, out = $STDOUT)
    @session = session
    @out = out
  end

  def hello
    @out.puts "hello"
  end
end

Test:

describe Session do
  let (:fake_stdout) {double("$STDOUT", :puts => true)}
  let (:interface) {instance_double("Interface", :out => "fake_stdout")}
  let (:session) { Session.new }

  describe "#new" do
    it "creates an instance of Session" do
      expect(session).to be_an_instance_of(Session)
    end
  end
end

This throws private method 'puts' called for nil:NilClass. It seems it's not seeing the fake_stdout with its specified :puts as out. I tried tying it with allow(Interface).to receive(:new).with(session).and_return(interface), but that changed nothing. How do I get the tested Session class to see the double/instance double and pass the test?

spectre6000
  • 455
  • 4
  • 17

1 Answers1

0

I think, this is not really problem with stubbing, but the general approach. When writing your unit tests for some class, you should stick to functionality of that class and eventually to API it sees. If you're stubbing "internal" out of Interface - it's already to much for specs of Session.

What Session really sees, is Interfaces public hello method, thus Session spec, should not be aware of internal implementation of it (that it is @out.puts "hello"). The only thing you should really focus is that, the hello method has been called. On the other hand, ensuring that the put is called for hello should be described in specs for Interface.

Ufff... That's long introduction/explanation, but how to proceed then? (known as show me the code! too ;)).

Having said, that Session.new should be aware only of Interfaces hello method, it should trust it works properly, and Sessions spec should ensure that the method is called. For that, we'll use a spy. Let's get our hand dirty!

RSpec.describe Session do
  let(:fake_interface) { spy("interface") }
  let(:session)        { Session.new }

  before do
    allow(Interface).to receive(:new).and_return(fake_interface)
  end

  describe "#new" do
    it "creates an instance of Session" do
      expect(session).to be_an_instance_of(Session) # this works now!
    end

    it "calls Interface's hello method when initialized" do
      Session.new
      expect(fake_interface).to have_received(:hello)
    end
  end
end

A test spy is a function that records arguments, return value, the value of this and exception thrown (if any) for all its calls.

This is taken from SinonJS (which is the first result when googling for "what is test spy"), but explanation is accurate.

How does this work?

Session.new
expect(fake_interface).to have_received(:hello)

First of all, we're executing some code, and after that we're asserting that expected things happened. Conceptually, we want to be sure, that during Session.new, the fake_interface have_received(:hello). That's all!

Ok, but I need another test ensuring that Interfaces method is called with specific argument.

Ok, let's test that!

Assuming the Session looks like:

class Session
  def initialize
    @interface = Interface.new(self)
    @interface.hello
    @interface.say "Something More!"
  end
end

We want to test say:

RSpec.describe Session do
  describe "#new" do
    # rest of the code

    it "calls interface's say_something_more with specific string" do
      Session.new
      expect(fake_interface).to have_received(:say).with("Something More!")
    end
  end
end

This one is pretty straightforward.

One more thing - my Interface takes a Session as an argument. How to test that the interface calls sessions method?

Let's take a look at sample implementation:

class Interface
  # rest of the code

  def do_something_to_session
    @session.a_session_method
  end
end

class Session
  # ...

  def another_method
    @interface.do_something_to_session
  end

  def a_session_method
    # some fancy code here
  end
end

It won't be much surprise, if I say...

RSpec.describe Session do
  # rest of the code

  describe "#do_something_to_session" do
    it "calls the a_session_method" do
      Session.new.another_method
      expect(fake_interface).to have_received(:do_something_to_session)
    end
  end
end

You should check, if Sessions another_method called interfaces do_something_to_session method.

If you test like this, you make the tests less fragile to future changes. You might change an implementation of Interface, that it doesn't rely on put any more. When such change is introduced - you have to update the tests of Interface only. Session knows only the proper method is called, but what happens inside? That's the Interfaces job...

Hope that helps! Please, take a look at another example of spy in my other answer.

Good luck!

Community
  • 1
  • 1
Paweł Dawczak
  • 9,519
  • 2
  • 24
  • 37
  • While spies seem to be a stylistic choice (according to my understanding of the RSpec documentation), this is the first and the best explanation of stubbing I have seen in the RSpec 3.X vernacular. You are right that my approach is lacking (many changes have been made to that effect), but you would be helping a lot of people if you were to write this into some stand alone piece solely on stubbing with this level of context, detail, and explanation. I've been stuck on trying to make sense of stubbing for more than a month, and this is what made it click. – spectre6000 Apr 03 '15 at 16:52