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 Interface
s 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 Interface
s hello
method, it should trust it works properly, and Session
s 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 Interface
s 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 session
s 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 Session
s another_method
called interface
s 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 Interface
s job...
Hope that helps! Please, take a look at another example of spy
in my other answer.
Good luck!