How to properly stub doubles - ruby

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?

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!

Related

Rspec 3.0 How to mock a method replacing the parameter but with no return value?

I've searched a lot and just cannot figure this out although it seems basic. Here's a way simplified example of what I want to do.
Create a simple method that does something but doesn't return anything, such as:
class Test
def test_method(param)
puts param
end
test_method("hello")
end
But in my rspec test I need to pass a different parameter, such as "goodbye" instead of "hello." I know this has to do with stubs and mocks, and I've looking over the documentation but can't figure it out: https://relishapp.com/rspec/rspec-mocks/v/3-0/docs/method-stubs
If I do:
#test = Test.new
allow(#test).to_receive(:test_method).with("goodbye")
it tells me to stub out a default value but I can't figure out how to do it correctly.
Error message:
received :test_method with unexpected arguments
expected: ("hello")
got: ("goodbye")
Please stub a default value first if message might be received with other args as well.
I am using rspec 3.0, and calling something like
#test.stub(:test_method)
is not allowed.
How to set a default value that is explained at
and_call_original can configure a default response that can be overriden for specific args
require 'calculator'
RSpec.describe "and_call_original" do
it "can be overriden for specific arguments using #with" do
allow(Calculator).to receive(:add).and_call_original
allow(Calculator).to receive(:add).with(2, 3).and_return(-5)
expect(Calculator.add(2, 2)).to eq(4)
expect(Calculator.add(2, 3)).to eq(-5)
end
end
Source where I came to know about that can be found at https://makandracards.com/makandra/30543-rspec-only-stub-a-method-when-a-particular-argument-is-passed
For your example, since you don't need to test the actual result of test_method, only that puts gets called in it passing in param, I would just test by setting up the expectation and running the method:
class Test
def test_method(param)
puts param
end
end
describe Test do
let(:test) { Test.new }
it 'says hello via expectation' do
expect(test).to receive(:puts).with('hello')
test.test_method('hello')
end
it 'says goodbye via expectation' do
expect(test).to receive(:puts).with('goodbye')
test.test_method('goodbye')
end
end
What it seems you're attempting to do is set up a test spy on the method, but then I think you're setting up the method stub one level too high (on test_method itself instead of the call to puts inside test_method). If you put the stub on the call to puts, your tests should pass:
describe Test do
let(:test) { Test.new }
it 'says hello using a test spy' do
allow(test).to receive(:puts).with('hello')
test.test_method('hello')
expect(test).to have_received(:puts).with('hello')
end
it 'says goodbye using a test spy' do
allow(test).to receive(:puts).with('goodbye')
test.test_method('goodbye')
expect(test).to have_received(:puts).with('goodbye')
end
end

Share state between test classes and tested classes

I'm experimenting with RSpec.
Since I don't like mocks, I would like to emulate a console print using a StringIO object.
So, I want to test that the Logger class writes Welcome to the console. To do so, my idea was to override the puts method used inside Logger from within the spec file, so that nothing actually changes when using Logger elsewhere.
Here's some code:
describe Logger do
Logger.class_eval do
def puts(*args)
???.puts(*args)
end
end
it 'says "Welcome"' do
end
Doing this way, I need to share some StringIO object (which would go where the question marks are now) between the Logger class and the test class.
I found out that when I'm inside RSpec tests, self is an instance of Class. What I thought initially was to do something like this:
Class.class_eval do
attr_accessor :my_io
#my_io = StringIO.new
end
and then replace ??? with Class.my_io.
When I do this, a thousand bells ring in my head telling me it's not a clean way to do this.
What can I do?
PS: I still don't get this:
a = StringIO.new
a.print('a')
a.string # => "a"
a.read # => "" ??? WHY???
a.readlines # => [] ???
Still: StringIO.new('hello').readlines # => ["hello"]
To respond to your last concern, StringIO simulates file behavior. When you write/print to it, the input cursor is positioned after the last thing you wrote. If you write something and want to read it back, you need to reposition yourself (e.g. with rewind, seek, etc.), per http://ruby-doc.org/stdlib-1.9.3/libdoc/stringio/rdoc/StringIO.html
In contrast, StringIO.new('hello') establishes hello as the initial contents of the string while leaving in the position at 0. In any event, the string method just returns the contents, independent of position.
It's not clear why you have an issue with the test double mechanism in RSpec.
That said, your approach for sharing a method works, although:
The fact that self is an anonymous class within RSpec's describe is not really relevant
Instead of using an instance method of Class, you can define your own class and associated class method and "share" that instead, as in the following:
class Foo
def self.bar(arg)
puts(arg)
end
end
describe "Sharing stringio" do
Foo.class_eval do
def self.puts(*args)
MyStringIO.my_io.print(*args)
end
end
class MyStringIO
#my_io = StringIO.new
def self.my_io ; #my_io ; end
end
it 'says "Welcome"' do
Foo.bar("Welcome")
expect(MyStringIO.my_io.string).to eql "Welcome"
end
end
Logger already allows the output device to be specified on construction, so you can easily pass in your StringIO directly without having to redefine anything:
require 'logger'
describe Logger do
let(:my_io) { StringIO.new }
let(:log) { Logger.new(my_io) }
it 'says welcome' do
log.error('Welcome')
expect(my_io.string).to include('ERROR -- : Welcome')
end
end
As other posters have mentioned, it's unclear whether you're intending to test Logger or some code that uses it. In the case of the latter, consider injecting the logger into the client code.
The answers to this SO question also show several ways to share a common Logger between clients.

Working around the need for partial mocks

From time to time I run into the situation that I want to use partial mocks of class methods in my tests. Currently, I'm working with minitest which does not support this (probably because it's not a good idea in the first place...).
An example:
class ImportRunner
def self.run *ids
ids.each { |id| ItemImporter.new(id).import }
end
end
class ItemImporter
def initialize id
#id = id
end
def import
do_this
do_that
end
private
def do_this
# do something with fetched_data
end
def do_that
# do something with fetched_data
end
def fetched_data
#fetched_data ||= DataFetcher.get #id
end
end
I want to test the ImportRunner.run method in isolation (mainly because ItemImporter#import is slow/expensive). In rspec I would have written a test like this:
it 'should do an import for each id' do
first_importer = mock
second_importer = mock
ItemImporter.should_receive(:new).with(123).and_return(first_importer)
first_importer.should_receive(:import).once
ItemImporter.should_receive(:new).with(456).and_return(second_importer)
second_importer.should_receive(:import).once
ImportRunner.run 123, 456
end
First part of the question: Is it possible to do something similar in minitest?
Second part of the question: Is object collaboration in the form
collaborator = SomeCollaborator.new a_param
collaborator.do_work
bad design? If so, how would you change it?
What you are asking for is almost possible in straight Minitest. Minitest::Mock doesn't support partial mocking, so we attempt to do this by stubbing ItemImporter's new method and returning a lambda that calls a mock that returns mocks instead. (Mocks within a mock: Mockception)
def test_imports_for_each_id
# Set up mock objects
item_importer = MiniTest::Mock.new
first_importer = MiniTest::Mock.new
second_importer = MiniTest::Mock.new
# Set up expectations of calls
item_importer.expect :new, first_importer, [123]
item_importer.expect :new, second_importer, [456]
first_importer.expect :import, nil
second_importer.expect :import, nil
# Run the import
ItemImporter.stub :new, lambda { |id| item_importer.new id } do
ImportRunner.run 123, 456
end
# Verify expectations were met
# item_importer.verify
first_importer.verify
second_importer.verify
end
This will work except for calling item_importer.verify. Because that mock will return other mocks, the process of verifying all the expectations were met will call additional methods on the first_importer and second_importer mocks, causing them to raise. So while you can get close, you can't replicate your rspec code exactly. To do that you will have to use a different mocking library that supports partial mocks like RR.
If that code looks ugly to you, don't worry, it is. But that isn't the fault of Minitest, its the fault of conflicting responsibilities within the test. Like you said, this probably isn't a good idea. I don't know what this test is supposed to prove. It looks to be specifying the implementation of your code, but it isn't really communicating the expected behavior. This is what some folks call "over-mocked".
Mocks and stubs are important and useful tools in the hands of a developer, but it’s easy to get carried away. Besides lending a false sense of security, over-mocked tests can also be brittle and noisy. - Rails AntiPatterns
I would rethink what you are trying to accomplish with this test. Minitest is helping you out here by making the design choice that ugly things should look ugly.
You could use the Mocha gem. I am also using MiniTest in most of my tests, and using Mocha to mock and stub methods.

How to test method that delegates to the initiation of another class with rspec?

How would you go about testing this with rspec?
class SomeClass
def map_url(size)
GoogleMap.new(point: model.location.point, size: size).map_url
end
end
The fact that your test seems "very coupled and brittle to mock" is a sign that the code itself is doing too many things at once.
To highlight the problem, look at this implementation of map_url, which is meaningless (returning "foo" for any size input) and yet passes your tests:
class SomeClass
def map_url(size)
GoogleMap.new.map_url
GoogleMap.new(point: model.location.point, size: size)
return "foo"
end
end
Notice that:
A new map is being initiated with the correct arguments, but is not contributing to the return value.
map_url is being called on a newly-initiated map, but not the one initiated with the correct arguments.
The result of map_url is not being returned.
I'd argue that the problem is that the way you have structured your code makes it look simpler than it actually is. As a result, your tests are too simple and thus fall short of fully covering the method's behaviour.
This comment from David Chelimsky seems relevant here:
There is an old guideline in TDD that suggests that you should listen to
your tests because when they hurt there is usually a design problem.
Tests are clients of the code under test, and if the test hurts, then so
do all of the other clients in the codebase. Shortcuts like this quickly
become an excuse for poor designs. I want it to stay painful because it
should hurt to do this.
Following this advice, I'd suggest first splitting the code into two separate methods, to isolate concerns:
class SomeClass
def new_map(size)
GoogleMap.new(point: model.location.point, size: size)
end
def map_url(size)
new_map(size).map_url
end
end
Then you can test them separately:
describe SomeClass do
let(:some_class) { SomeClass.new }
let(:mock_map) { double('map') }
describe "#new_map" do
it "returns a GoogleMap with the correct point and size" do
map = some_class.new_map('300x600')
map.point.should == [1,2]
map.size.should == '300x600'
end
end
describe "#map_url" do
before do
some_class.should_receive(:new_map).with('300x600').and_return(mock_map)
end
it "initiates a new map of the right size and call map_url on it" do
mock_map.should_receive(:map_url)
some_class.map_url('300x600')
end
it "returns the url" do
mock_map.stub(map_url: "http://www.example.com")
some_class.map_url('300x600').should == "http://www.example.com"
end
end
end
The resulting test code is a longer and there are 3 specs rather than two, but I think it more clearly and cleanly separates the steps involved in your code, and covers the method behaviour completely. Let me know if this makes sense.
So this is how I did it, it feels very coupled and brittle to mock it like this. Suggestions?
describe SomeClass do
let(:some_class) { SomeClass.new }
describe "#map_url" do
it "should instantiate a GoogleMap with the correct args" do
GoogleMap.should_receive(:new).with(point: [1,2], size: '300x600') { stub(map_url: nil) }
some_class.map_url('300x600')
end
it "should call map_url on GoogleMap instance" do
GoogleMap.any_instance.should_receive(:map_url)
some_class.map_url('300x600')
end
end
end

Ruby Double (RR) How do I set up expectations for a block of method calls passed to block argument method?

In my code I have code similar to the following contrived example.
class Excel
def self.do_tasks
with_excel do |excel|
delete_old_exports
export_images(excel)
export_documents(excel)
end
end
def with_excel
excel = WIN32OLE.connect('Excel.Application')
begin
yield excel
ensure
excel.close()
end
end
end
Now, I want to write a test for the 'do_tasks' method, where I set up expectations for the method calls and see if those expectations are fulfilled.
I tried the following approach (with shoulda-context and test-unit). However,the expectations fail for the three last mocks (the mocks do not get called).
class ExcelTest < ActiveSupport::TestCase
should "call the expected methods" do
mock.proxy(Excel).with_excel
mock(Excel).delete_old_exports
mock(Excel).export_images.with_any_args
mock(Excel).export_documents.with_any_args
Excel.do_tasks
end
end
Any pointers on how to test this sort of code would be much appreciated!
An older question, but I've just been doing some work on some similar code with rr and thought I'd throw in an answer.
The following test will do what you asked (using RR and TestUnit):
describe Excel do
describe '.do_tasks' do
let(:excel_ole) { mock!.close.subject }
before do
stub(WIN32OLE).connect('Excel.Application') { excel_ole }
mock(Excel).delete_old_exports
mock(Excel).export_images(excel_ole)
mock(Excel).export_documents(excel_ole)
end
it 'calls the expected methods' do
Excel.do_tasks
assert_received(Excel) { |subject| subject.delete_old_exports }
end
end
end
It uses RR's "spy" doubles - see https://github.com/rr/rr#spies
However, in the case of the sample code you provided, the fact that the methods you want to test are inside a block is an implementation detail and shouldn't be implicitly tested (this can lead to brittle tests). The test above shows this, the with_excel method is not mocked (incidentally, this should be defined as self.with_excel for the code to work). The implementation could be refactored so that the WIN32OLE initialisation and teardown happens inline in the .do_tasks method and the test would still pass.
On another note, it may be a side effect of the contrived example, but in general it's a bad idea to test non-public methods. The methods delete_old_exports, export_images and export_documents look like they should perhaps be factored out to collaborators.

Resources