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 ?
CodePudding user response:
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.