rspec should_receive is not working but expect is working - ruby-on-rails

I have class like below
#bank.rb
class Bank
def transfer(customer1, customer2, amount_to_transfer)
if customer1.my_money >= amount_to_transfer
customer1.my_money -= amount_to_transfer
customer2.my_money += amount_to_transfer
else
return "Insufficient funds"
end
end
end
class Customer
attr_accessor :my_money
def initialize(amount)
self.my_money = amount
end
end
And my spec file looks as below:
#spec/bank_spec.rb
require './spec/spec_helper'
require './bank'
describe Bank do
context "#transfer" do
it "should return insufficient balance if transferred amount is greater than balance" do
customer1 = Customer.new(500)
customer2 = Customer.new(0)
customer1.stub(:my_money).and_return(1000)
customer2.stub(:my_money).and_return(0)
expect(Bank.new.transfer(customer1, customer2, 2000)).to eq("Insufficient funds")
expect(customer1).to have_received(:my_money) # This works
customer1.should_receive(:my_money) #throws error
end
end
end
As per https://relishapp.com/rspec/rspec-mocks/v/2-14/docs/message-expectations both expect and should_receive are same but expect is more readable than should_receive. But why it is failing? Thanks in advance.

place this line:
customer1.should_receive(:my_money)
before
expect(Bank.new.transfer(customer1, customer2, 2000)).to eq("Insufficient funds")
expect to have_received and should_receive have diffent meaning
expect to have_received passes if object already received expected method call while
should_receive passes only if object will receive expected method call in future (in scope of current testcase)
if you would write
expect(customer1).to receive(:my_money)
instead of
expect(customer1).to have_received(:my_money)
it would fail too. Unless you place it before the line which calls this method.

Related

Rspec mocks and stubs confuse with expect

I have confuse when use mocks and stubs in rspec on rails. I have test like below
require 'rails_helper'
class Payment
attr_accessor :total_cents
def initialize(payment_gateway, logger)
#payment_gateway = payment_gateway
#logger = logger
end
def save
response = #payment_gateway.charge(total_cents)
#logger.record_payment(response[:payment_id])
end
end
class PaymentGateway
def charge(total_cents)
puts "THIS HITS THE PRODUCTION API AND ALTERS PRODUCTION DATA. THAT'S BAD!"
{ payment_id: rand(1000) }
end
end
class LoggerA
def record_payment(payment_id)
puts "Payment id: #{payment_id}"
end
end
describe Payment do
it 'records the payment' do
payment_gateway = double()
allow(payment_gateway).to receive(:charge).and_return(payment_id: 1234)
logger = double('LoggerA')
expect(logger).to receive(:record_payment).with(1234)
payment = Payment.new(payment_gateway, logger)
payment.total_cents = 1800
payment.save
end
end
Ok when I run rspec it works, no problem, but when I try to move expect to last line like below:
payment = Payment.new(payment_gateway, logger)
payment.total_cents = 1800
payment.save
expect(logger).to receive(:record_payment).with(1234)
and I try to run rpsec, it fail, I dont know why expect is last line will fail, I thought that expect always puts in last line before we run something to get result to test. Anyone can explain for me ?
expect(sth).to receive sets a message expectation which is to be satisfied between the call and end of the test, and that expectation is verified after the test finishes. When you move the expect to the last line, expectation is set just at the end of the test and no code is executed to satisfy it so it fails. Unfortunately it means breaking the prepare-execute-test order.
Which is why you should really rarely use expect.to receive and replace it with allow.to receive with expect.to have_received
# prepare
allow(logger).to receive(:record_payment)
# execute
..
# test
expect(logger).to have_received(:record_payment).with(1234)
allow.to receive sets up a mock proxy which starts tracing received messages which then can be explicitly verified by expect.to have_received. Some objects automatically sets their mock proxies, for example you don't need allow.to receive for doubles with predefined responses or spies. In your case, you could write the test like:
payment_gateway = double
allow(payment_gateway).to receive(:charge).and_return(payment_id: 1234)
logger = double('LoggerA', record_payment: nil)
payment = Payment.new(payment_gateway, logger)
payment.total_cents = 1800
payment.save
expect(logger).to have_received(:record_payment).with(1234)
Other notes
I strongly recommend using verifiable_doubles, which will protect you from false positives:
payment_gateway = instance_double(PaymentGateway)
allow(payment_gateway).to receive(:charge).and_return(payment_id: 1234)
This test will now raise an exception if there is no charge method defined on PaymentGateway class - protecting you from your tests passing even in case you rename that method but forgot to rename it in the test and implementation.

Getting Rspec unit test coverage with Rails and PostgreSQL

I am trying to write a unit test for the following model concern...
require 'active_support/concern'
module Streamable
extend ActiveSupport::Concern
def stream_query_rows(sql_query, options = 'WITH CSV HEADER')
conn = ActiveRecord::Base.connection.raw_connection
conn.copy_data("COPY (#{sql_query}) TO STDOUT #{options};") do
binding.pry
while row = conn.get_copy_data
binding.pry
yield row
end
end
end
end
So far I have battling this with the following spec...
context 'streamable' do
it 'is present' do
expect(described_class.respond_to?(:stream_query_rows)).to eq(true)
end
context '#stream_query_rows', focus: true do
let(:sql_query) { 'TESTQRY' }
let(:sql_query_options) { 'WITH CSV HEADER' }
let(:raw_connection) do
Class.new do
def self.copy_data(args)
yield
end
def self.get_copy_data
return Proc.new { puts 'TEST' }
end
end
end
before do
allow(ActiveRecord::Base).to receive_message_chain(:connection, :raw_connection).and_return(raw_connection)
described_class.stream_query_rows(sql_query)
end
it 'streams data from the db' do
expect(raw_connection).to receive(:copy_data).with("COPY (#{sql_query}) TO STDOUT #{sql_query_options};")
end
end
end
While I can get the first expect to pass, meaning, I can trigger the first binding.pry, no matter what I try, I can not seem to get past the second.
This is the error...
LocalJumpError:
no block given (yield)
I am only trying to unit test this and ideally not hit the db, only testing the communication of the objects. This also, can and will be used in many models as an option for streaming data.
Reference article: https://shift.infinite.red/fast-csv-report-generation-with-postgres-in-rails-d444d9b915ab
Does anyone have an pointers on how to finish this stub and or adjust the spec so I have the following block covered?
while row = conn.get_copy_data
binding.pry
yield row
end
ANSWER
After reviewing the comments and suggestions below, I was able to refactor the spec and now have 100% coverage.
context '#stream_query_rows' do
let(:sql_query) { 'TESTQRY' }
let(:sql_query_options) { 'WITH CSV HEADER' }
let(:raw_connection) { double('RawConnection') }
let(:stream_query_rows) do
described_class.stream_query_rows(sql_query) do
puts sql_query
break
end
end
before do
allow(raw_connection).to receive(:copy_data).with("COPY (#{sql_query}) TO STDOUT #{sql_query_options};"){ |&block| block.call }
allow(raw_connection).to receive(:get_copy_data).and_return(sql_query)
allow(ActiveRecord::Base).to receive_message_chain(:connection, :raw_connection).and_return(raw_connection)
end
it 'streams data from the db' do
expect(raw_connection).to receive(:copy_data).with("COPY (#{sql_query}) TO STDOUT #{sql_query_options};")
stream_query_rows
end
it 'yields correct data' do
expect { stream_query_rows }.to output("#{sql_query}\n").to_stdout_from_any_process
end
end
Like the error says, you're yielding, but you haven't supplied a block for it to call.
If your method expects a block, then you need to supply one when you call it.
To do that, you need to change this line:
described_class.stream_query_rows(sql_query)
to something like this:
described_class.stream_query_rows(sql_query) { puts "this is a block" }

Test if a method is called inside a module method

I have the following in my module:
module SimilarityMachine
...
def answers_similarity(answer_1, answer_2)
if answer_1.compilation_error? && answer_2.compilation_error?
return compiler_output_similarity(answer_1, answer_2)
elsif answer_1.compilation_error? || answer_2.compilation_error?
return source_code_similarity(answer_1, answer_2)
else
content_sim = source_code_similarity(answer_1, answer_2)
test_cases_sim = test_cases_output_similarity(answer_1, answer_2)
answers_formula(content_sim, test_cases_sim)
end
end
...
end
I would like to test these "if conditions", to ensure that the right methods are called (all these methods are from SimilarityMachine module). To do that, I have:
describe SimilarityMachine do
describe '#answers_similarity' do
subject { answers_similarity(answer_1, answer_2) }
let(:answer_1) { create(:answer, :invalid_content) }
context "when both answers have compilation error" do
let(:answer_2) { create(:answer, :invalid_content) }
it "calls compiler_output_similarity method" do
expect(described_class).to receive(:compiler_output_similarity)
subject
end
end
end
With both answers created I go to the right if (the first, and I'm sure of that because I tested before). However, my result is:
1) SimilarityMachine#answers_similarity when both answers have compilation error calls compiler_output_similarity method
Failure/Error: expect(described_class).to receive(:compiler_output_similarity)
(SimilarityMachine).compiler_output_similarity(*(any args))
expected: 1 time with any arguments
received: 0 times with any arguments
What am I doing wrong?
I would check out Testing modules in rspec other questions related to testing modules.
I'm not completely clear on this, but in general, modules don't receive method calls. They are collections of methods that have to be "mixed in" through the extend method and the like.
Here's an example how to test a module method in isolation, taken from https://semaphoreci.com/community/tutorials/testing-mixins-in-isolation-with-minitest-and-rspec:
describe FastCar
before(:each) do
#test_obj = Object.new
#test_obj.extend(Speedable)
end
it "reports the speed" do
expect(#test_obj.speed).to eq "This car runs super fast!"
end
end

How to delete an entire array in Ruby and test with RSpec

I'm fairly new to Ruby and am currently taking a full stack course. For one of my projects we are building an addressbook. I have set up how to add an entry to the addressbook, however, I can't seem to figure out how to delete an entry (I make an attempt with the remove_entry method in the AddressBook class below but am not having any luck). We are also supposed to test first with RSpec, have the test fail and then write some code to get it to pass. If I didn't include all the info needed for this question let me know (rookie here). Anyway, here is what I have so far:
RSpec
context ".remove_entry" do
it "removes only one entry from the address book" do
book = AddressBook.new
entry = book.add_entry('Ada Lovelace', '010.012.1815', 'augusta.king#lovelace.com')
book.remove_entry(entry)
expect(entry).to eq nil
end
end
AddressBook class
require_relative "entry.rb"
class AddressBook
attr_accessor :entries
def initialize
#entries = []
end
def add_entry(name, phone, email)
index = 0
#entries.each do |entry|
if name < entry.name
break
end
index += 1
end
#entries.insert(index, Entry.new(name, phone, email))
end
def remove_entry(entry)
#entries.delete(entry)
end
end
Entry class
class Entry
attr_accessor :name, :phone_number, :email
def initialize(name, phone_number, email)
#name = name
#phone_number = phone_number
#email = email
end
def to_s
"Name: #{#name}\nPhone Number: #{#phone_number}\nEmail: #{#email}"
end
end
When testing my code with RSpec I receive the following error message:
.....F
Failures:
1) AddressBook.remove_entry removes only one entry from the address book
Failure/Error: expect(entry).to eq nil
expected: nil
got: [#<Entry:0x00000101bc82f0 #name="Ada Lovelace", #phone_number="010.012.1815", #email="augusta.king#lovelace.com">]
(compared using ==)
# ./spec/address_book_spec.rb:49:in `block (3 levels) in <top (required)>'
Finished in 0.02075 seconds (files took 0.14221 seconds to load)
6 examples, 1 failure
Failed examples:
rspec ./spec/address_book_spec.rb:44 # AddressBook.remove_entry removes only one entry from the address book
Just test that the book.entries association is empty:
expect(book.entries).to be_empty
As book is a local variable in your test, you will not get a false negative result if you keep your test atomic. Some best practices on rspec.
Edit:
You can also check the entry was not in the set:
expect(book.entries.index(entry)).to be_nil
or test the change of the array length with:
expect { book.remove_entry(entry) }.to change{book.entries.count}.by(-1)
If you wonder for the be_xxx syntax sugar, if the object respond to xxx?, then you can use be_xxx in your tests (predicate matchers)
I think your expect has an issue. The entry variable is not set to nil, but the entry inside book would be nil.
I think something like this would work better:
expect(book.entries.find { |e| e.name == "Ada Lovelace" }).to eq nil
Better still, your AddressBook could have its own find method, which would make the expect param much nicer, like book.find(:name => "Ada Lovelace").
Finally, I would also put an expect call before the remove_entry call, to make sure its result equals entry.

RSpec: Mock not working for instance method and multiple calls

I'm trying to mock PriceInspector#get_latest_price below to test OderForm. There are two orders passed in, hence, I need to return two different values when mocking PriceInspector#get_latest_price. It all works fine with the Supplier model (ActiveRecord) but I can't run a mock on the PriceInspector class:
# inside the test / example
expect(Supplier).to receive(:find).and_return(supplier_1) # first call, works
expect(PriceInspector).to receive(:get_latest_price).and_return(price_item_1_supplier_1) # returns nil
expect(Supplier).to receive(:find).and_return(supplier_2) # second call, works
expect(PriceInspector).to receive(:get_latest_price).and_return(price_item_2_supplier_1) # returns nil
class OrderForm
include ActiveModel::Model
def initialize(purchaser)
#purchaser = purchaser
end
def submit(orders)
orders.each do |supplier_id, order_items|
#supplier = Organization.find(supplier_id.to_i)
#order_item = OrderItem.save(
price_unit_price: PriceInspector.new(#purchaser).get_latest_price.price_unit_price
)
[...]
end
end
end
class PriceInspector
def initialize(purchaser)
#purchaser = purchaser
end
def get_latest_price
[...]
end
end
Edit
Here's the updated test code based on Bogieman's answer:
before(:each) do
expect(Organization).to receive(:find).and_return(supplier_1, supplier_2)
price_inspector = PriceInspector.new(purchaser, item_1)
PriceInspector.stub(:new).and_return price_inspector
expect(price_inspector).to receive(:get_latest_price).and_return(price_item_1_supplier_1)
expect(price_inspector).to receive(:get_latest_price).and_return(price_item_2_supplier_2)
end
it "saves correct price_unit_price for first OrderItem", :focus do
order_form.submit(params)
expect(OrderItem.first.price_unit_price).to be_within(0.01).of(price_item_1_supplier_1.price_unit_price)
end
I think this should fix the instance method problem and allow you to check for the two different returns (provided you pass in the purchaser or a double) :
price_inspector = PriceInspector.new(purchaser)
PriceInspector.stub(:new).and_return price_inspector
expect(price_inspector).to receive(:get_latest_price).and_return(price_item_1_supplier_1)
expect(price_inspector).to receive(:get_latest_price).and_return(price_item_2_supplier_1)

Resources