RSpec - how to test if object sends messages to self in #initialize - ruby

After reading this question I really do not like the answer.
Rails / RSpec: How to test #initialize method?
Maybe I am having a third scenario. This is what I have now, inspired by second code from that answer.
# Picture is collection of SinglePictures with same name and filename,
# but different dimensions
class Picture
attr_accessor :name, :filename
attr_reader :single_pics, :largest_width
def initialize(name, filename, dimensions=nil)
#largest_width = 0
#single_pics = {}
add_single_pics(dimensions) if dimensions
end
def add_single_pics(max_dimension)
# logic
end
end
describe '#initialize' do
it 'should not call add_single_pics if dimensions is not given' do
subject = Picture.new('Test Picture', 'Test-Picture')
expect(subject.largest_width).to eq 0
end
it 'should call add_single_pics if dimensions are given' do
subject = Picture.new('Test Picture', 'Test-Picture', 1920)
expect(subject.largest_width).to eq 1920
end
end
I really don't like this because I am testing the functionality of add_single_pics in #initialize tests. I would like to write somehow this in spec:
expect(subject).not_to have_received(:add_single_pics)
expect(subject).to have_received(:add_single_pics)
But I get
Expected to have received add_single_pics, but that object is not a spy
or method has not been stubbed.
Can I fix this somehow?

Spies are an alternate type of test double that support this pattern
by allowing you to expect that a message has been received after the
fact, using have_received.
https://relishapp.com/rspec/rspec-mocks/v/3-5/docs/basics/spies
Only spy object can store the method calls. To test your real class in the way that you want, you have to use expect_any_instance_of statement before the class will be initialized:
expect_any_instance_of(Picture).to receive(:add_single_pics)
Picture.new('Test Picture', 'Test-Picture')
In this case your add_single_pics method will be called, but its logic will not be run, if you need to run it you need to call the and_call_original method on the matcher:
expect_any_instance_of(Picture).to receive(:add_single_pics).and_call_original

Related

How to stub class instantiated inside tested class in rspec

I have problem stubbing external api, following is the example
require 'rspec'
require 'google/apis/storage_v1'
module Google
class Storage
def upload file
puts '#' * 90
puts "File #{file} is uploaded to google cloud"
end
end
end
class UploadWorker
include Sidekiq::Worker
def perform
Google::Storage.new.upload 'test.txt'
end
end
RSpec.describe UploadWorker do
it 'uploads to google cloud' do
google_cloud_instance = double(Google::Storage, insert_object: nil)
expect(google_cloud_instance).to receive(:upload)
worker = UploadWorker.new
worker.perform
end
end
I'm trying to stub Google::Storage class. This class is instantiated inside the object being tested. How can I verify the message expectation on this instance?
When I run above example, I get following output, and it seems logical, my double is not used by tested object
(Double Google::Storage).upload(*(any args))
expected: 1 time with any arguments
received: 0 times with any arguments
I'm new to Rspec and having hard time with this, any help will be appreciated.
Thanks!
Reaching for DI is always a good idea (https://stackoverflow.com/a/51401376/299774) but there are sometimes reasons you can't so it, so here's another way to stub it without changing the "production" code.
1. expect_any_instance_of
it 'uploads to google cloud' do
expect_any_instance_of(Google::Storage).to receive(:insert_object)
worker = UploadWorker.new
worker.perform
end
In case you just want to test that the method calls the method on any such objects.
2. bit more elaborated setup
In case you want to control or set up more expectations, you can do this
it 'uploads to google cloud' do
the_double = instance_double(Google::Storage)
expect(Google::Storage).to receive(:new).and_return(the_double)
# + optional `.with` in case you wanna assert stuff passed to the constructor
expect(the_double).to receive(:insert_object)
worker = UploadWorker.new
worker.perform
end
Again - Dependency Injection is clearer, and you should aim for it. This is presented as another possibility.
I would consider reaching for dependency injection, such as:
class UploadWorker
def initialize(dependencies = {})
#storage = dependencies.fetch(:storage) { Google::Storage }
end
def perform
#storage.new.upload 'test.txt'
end
end
Then in the spec you can inject a double:
storage = double
expect(storage).to receive(...) # expection
worker = UploadWorker.new(storage: storage)
worker.perform
If using the initializer is not an option then you could use getter/setter method to inject the dependency:
def storage=(new_storage)
#storage = new_storage
end
def storage
#storage ||= Google::Storage
end
and in the specs:
storage = double
worker.storage = storage

What is 'valid?' in RSpec? Where can I look at it?

I've attempted to create a model, which needs to pass a series of validation tests in RSpec. However, I constantly get the error
expected #<Surveyor::Answer:0x0055db58e29260 #question=#<Double Surveyor::Question>, #value=5> to respond to `valid?`
My understanding (from here) was that 'valid?' checks that no errors were added to the model. I can't find any errors, however the message above persists.
This is my model
module Surveyor
class Answer
attr_accessor :question, :value
def initialize(params)
#question = params.fetch(:question)
#value = params.fetch(:value)
end
end
end
And the class Question
module Surveyor
class Question
attr_accessor :title, :type
def initialize(params)
#title = params.fetch(:title, nil)
#type = params.fetch(:type)
end
end
end
And this is the test I am attempting to pass
RSpec.describe Surveyor::Answer, '03: Answer validations' do
let(:question) { double(Surveyor::Question, type: 'rating') }
context "question validation" do
context "when the answer has a question" do
subject { described_class.new(question: question, value: 5) }
it { should be_valid }
end
end
Is my understanding of 'valid?' correct? Am I able to look at 'valid?' and perhaps see where I'm going wrong?
RSpec doesn't actually have a matcher called be_valid, instead it has some dynamic predicate matchers:
For any predicate method, RSpec gives you a corresponding matcher. Simply prefix the
method with be_ and remove the question mark. Examples:
expect(7).not_to be_zero # calls 7.zero?
expect([]).to be_empty # calls [].empty?
expect(x).to be_multiple_of(3) # calls x.multiple_of?(3)
so by calling it { should be_valid }, your subject has to respond to a valid? method. If you're testing an ActiveRecord model, those have a valid? method, but your model does not. So, if you want to test that your Answer is valid, you need to decide "what is a valid answer?" and write a method that checks for those conditions. If you want an API similar to Rails model, you might be interested in using ActiveModel::Validations

Rspec: Difference between allow and allow_any_instance_of

I have a simple MySQL wrapper class which will run a query and return results.
class Rsql
def initialize(db)
#client = Mysql2::Client
#db = db
end
def execute_query()
client = #client.new(#db)
client.query("select 1")
end
end
I want to test some stuff involving the results of the query, but I don't want to actually connect to a database to get the results. I tried this test, but it doesn't work:
RSpec.describe Rsql do
it "does it" do
mock_database = double
rsql = Rsql.new(mock_database)
mock_mysql_client = double
allow(mock_mysql_client).to receive(:query).and_return({"1" => 1})
allow_any_instance_of(Mysql2::Client).to receive(:new).and_return(mock_mysql_client)
expect(rsql.execute_query).to eq({"1" => 1})
end
end
Replacing allow_any_instance_of() with allow() works. I was under the impression that allow_any_instance_of() was some kind of a global "pretend this class behaves in this way across the entire program" whereas allow() is for specific instances of a class.
Can someone explain this behavior to me? I'm new to Rspec, so I apologize if this answer is blatantly obvious. I tried searching for the answer, but I couldn't come up with the right search string to find one. Maybe I don't know enough to know when I've found it.
As of RSpec 3.3 , any_instance is deprecated and not recommended to use in your tests.
From the docs:
any_instance is the old way to stub or mock any instance of a class
but carries the baggage of a global monkey patch on all classes. Note
that we generally recommend against using this feature.
You should only need to use allow(some_obj) going forward and the documentation has some great examples (see here).
Such as:
RSpec.describe "receive_messages" do
it "configures return values for the provided messages" do
dbl = double("Some Collaborator")
allow(dbl).to receive_messages(:foo => 2, :bar => 3)
expect(dbl.foo).to eq(2)
expect(dbl.bar).to eq(3)
end
end
Edit, if you really want to use any_instance, do so like this:
(Mysql2::Client).allow_any_instance.to receive(:something)
Edit2, your exact stub doesn't work because you're not stubbing an instance, you're stubbing before the object is initialized. In that case you would do allow(Mysql2::Client).to receive(:new).
this Rsql class seems a service
class Rsql
def initialize(db)
#client = Mysql2::Client
#db = db
end
def execute_query()
client = #client.new(#db)
client.query("select 1")
end
end
lets create a test for it, now we should to test this function execute_query with subject ()
and to create clients in db we can use let! like this
let!(:client1) do
FactoryBot.create(...
with this we should not use double or something
require 'rails_helper'
RSpec.describe RsqlTest do
subject(:clients) do
Rsql.execute_query()
end
context 'select' do
let!(:client1) do
FactoryBot.create(...
end
it 'should return records' do
expect(clients).to include(client1)
end
end
end

How to properly stub doubles

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!

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

Resources