Rspec -- mocking a method in another class - ruby

I'm new to RSpec, so bear with me!
Here is my code.
Testfile:
before(:all) do
#package = Package.new("testing")
#param_source = "cat #{root}/file/test.json"
end
"it should update the version params appropriately" do
group_params = mock("params")
group_params.expects(:each).multiple_yields([["staging", #param_source]])
#package.update_version(group_params)
# Some assertions here
end
Class file:
class Package
def initialize(db_file)
# Some http options set for httparty
end
def update_version(group_params)
group_params.each do |environment, param_source|
group_json = JSON.parse(HTTParty.get(param_source, #http_cred))
# Bunch more stuff done here
end
Basically, I have this test.json that I want to use to verify that things are getting parsed correctly. This HTTParty.get call expects a GET call to be made though. Is there a way I can mock that with the param_source variable. Please let me know if I need to provide more information here. Thanks for the help!

Stub:
HTTParty.stub(get: file_content)
or mock:
HTTParty.should_receive(:get).with(foo, bar).and_return(file_content)
or stubbing in the new rspec syntax:
allow(HTTParty).to receive(:get).and_return(file_content)
or mocking in the new syntax:
expect(HTTParty).to receive(:get).with(foo, bar).and_return(file_content)

Related

How to write rspec with mocking

I want to write an rspec test by mocking this method. Should I break this method up, as it doing multiple things?
require 'yaml'
require_relative 'checkerror'
class Operations
    def initialize
        #check
  end
    def result (result_log: File.new('result.txt', 'a+'))
      if #check.errors.empty?
 result_log.write("#{#check.checker.file_path} :: No offensenses detected\n")
#checker is instance of CheckError class
  puts "#{#check.checker.file_path} :: No offensenses detected\n"
      else
        #check.errors.uniq.each do |err|  puts "#{#check.checker.file_path} : #{err}\n"
 result_log.write("#{#check.checker.file_path} : #{err}\n")
 end
end
result_log.close
end
  end
end
If #check.errors need to be stuubed with a value and check the execution block.
It's going to be awkward mocking the f object in your current implementation, due to this line:
f = File.new('result.txt', 'a+')
You'd need to write something weird in the rspec test, like:
allow(File).to receive(:new).with('result.txt', 'a+').and_return(mock_file)
So instead, I'd recommend using dependency injection to pass the file into the method. For example:
def check_result(results_log: File.new('result.txt', 'a+'))
if #errors.empty?
# ...
end
Now, your rspec test can look something like this:
let(:results_log) { Tempfile.new }
it "prints errors to log file" do
wharever_this_object_is_called.check_result(result_log: results_log)
expect(result_log.read).to eq("checker_file_path.txt :: No offences detected\n")
end

How to test helper with params

I tried to test a method in helper with params[:a]. I'm not using Rspec so don't know how to solve it.
In helper file:
def get_commands(filter)
order_by = params[:order_by]
something else(filter)
end
and the test file:
test 'get_commands works' do
filter = something
res = get_commands(filter)
end
It shows: NameError: undefined local variable or method `params'
and it also doesn't work if I just add
params[:order_by]='desc'
A pretty generic approach is to use dependency injection.
First, change the method to accept params as an argument:
def get_commands(filter, params)
order_by = params[:order_by]
something else(filter)
end
Make sure to include this new argument in the controller when you call the method.
Then you can pass a mock parameter set in your test:
test 'get_commands works' do
filter = something
mock_params = ActionController::Parameters.new(order_by: :id)
res = get_commands(filter, mock_params)
# ... make your expectation about the result.
end
As a caveat, dependency injection is sometimes treated like an antipattern. Rails does have some built in helpers for testing controllers, see https://guides.rubyonrails.org/testing.html#functional-tests-for-your-controllers. But using dependency injection like this would definitely work, and is a bit simpler.

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

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

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.

Resources