Reputation: 1276
I'm trying to write a fairly straightforward test on a service object that handles an error. Rails version 5.2 and Rspec 3.8.
app/services/application_service.rb
class ApplicationService
def self.call(*args, &block)
new(*args, &block).call
end
end
app/services/basic_objects/create_payment.rb
module BasicObjects
class CreatePayment < ApplicationService
def initialize(args)
@transaction_id = args[:transaction_id]
end
def call
transaction = Transaction.find(@transaction_id)
payment = Payment.new(transaction: transaction)
payment.save
rescue CustomError => e
if /waiting/i === e.message
puts "Ignoring exception \"#{e.message}\" to prevent retry"
else
raise e
end
end
end
Here is the test
describe BasicObjects::CreatePayment, type: :model do
describe '#call' do
let(:params) { { transaction_id: "xyz" } }
context 'when there is a rescued error' do
before do
allow_any_instance_of(Payment)
.to receive(:save)
.and_raise(CustomError, "waiting")
end
it 'does not raise an error' do
expect { described_class.call(params) }.not_to raise_error
end
end
end
end
And it responds with this error:
expected no Exception, got #<ArgumentError: wrong number of arguments (given 1, expected 0)
But if I remove the params and instead expect(described_class.call).not_to raise_error
I get an error from not sending the argument
<BasicObjects::CreatePayment (class)> received :call with unexpected arguments
expected: ({:transaction_id=>"xyz"})
got: (no args)
So, then I remove with(params)
from the allow statement
context 'when there is a rescued error' do
before do
allow(described_class).to receive(:call).and_raise(CustomError, "waiting")
end
it 'does not raise an error' do
expect(described_class.call).not_to raise_error
end
end
And I get
ArgumentError:
wrong number of arguments (given 1, expected 0)
Any help is greatly appreciated
UPDATE
I have adjusted the call above to raise the error on a line in the class, rather than just in calling the class itself. However, I am still getting the same error: expected no Exception, got #<ArgumentError: wrong number of arguments (given 1, expected 0)>
Upvotes: 3
Views: 2138
Reputation: 21
I ran into a similar problem and the <ArgumentError: wrong number of arguments (given 1, expected 0)>
was very misleading and confusing.
Given your example, the problem is likely that your CustomError
class's initialize
method takes 0 argument and you are passing the "waiting"
string as part of your mock, ie. allow_any_instance_of(Payment).to receive(:save).and_raise(CustomError, "waiting")
.
The error message is actually complaining that you are passing the wrong number of arguments in your exception as your mocked exception is raised. So this should be fixed by removing "waiting"
:
allow_any_instance_of(Payment)
.to receive(:save)
.and_raise(CustomError)
Upvotes: 2
Reputation: 770
Wow, this was a confusing ride. Alright, so let's recap:
When you write allow(described_class).to receive(:call).and_raise(...)
, you're saying that this method:
class ApplicationService
def self.call(*args, &block)
new(*args, &block).call
end
end
should raise the error. When trying your code out locally, that's what actually happens.
Since the class call
isn't what's being tested, I would suggest you to test it like so, instead:
it 'does not raise an error' do
dbl = class_double(Transaction)
allow(dbl).to receive(:find).and_raise(CustomError, 'waiting')
expect { described_class.new(params).call }.not_to raise_error
end
You can't actually stub the instance method's call
, because by doing so you'll also remove the actual rescue
that you're testing.
Upvotes: 1
Reputation: 1167
Ok, there are a few things at play here, so let's go one by one.
Your create_payment.rb
defines an instance method call
on instances of CreatePayment
. Your test code, on the other hand, mocks and calls call
of described_class
— so, a class method.
You probably want to create an instance of CreatePayment
in your specs first.
CreatePayment.new(args).call
As defined CreatePayment
does not have a class method call
. But from the behaviour it seems it actually does.
When you mock a method that does not exist you get a following error message:
Failure/Error: allow(described_class).to receive(:call).with(params).and_raise(CustomError, "waiting")
SomeClass does not implement: call
That is not what happening, so check the ancestors: I managed to reproduce the error by adding a class method call
which accepts no args to the ancestor:
class SomeAncestor < Object
def self.call
raise "Abstract!"
end
end
class SomeClass < SomeAncestor;
end
This leads to the same error as you see:
Failure/Error: allow(described_class).to receive(:call).with(params).and_raise(CustomError, "waiting")
Wrong number of arguments. Expected 0, got 1.
Your ApplicationService
probably has some abstract implementation of call
class method.
The takeaway here is "Mocks should conform to the signatures of the original methods". But the root cause still seems to be the confusion between class and instance methods, so the first part still stands — CreatePayment
needs to be instantiated first, then called.
I'm leaving it here but it's not what's going on — edits made it clear.
RSpec.describe Whatever do
it 'does something' do
... described_class ...
end
end
If the first argument to an example group is a class, the class is exposed to each example in that example group via the described_class() method.
described_class
is Whatever
in my example above. In your spec it's nil
as you aren't passing a class (and you probably don't expect that). It means you are trying to mock and call methods of nil
.
And it really messes stuff up. I can't quite pinpoint the reason but this behaviour doesn't occur with normal objects. It actually is a common mistake, which is why you may see a warning from RSpec:
WARNING: An expectation of `:call` was set on `nil`. To allow expectations on `nil` and suppress this message, set `RSpec::Mocks.configuration.allow_message_expectations_on_nil` to `true`. To disallow expectations on `nil`, set `RSpec::Mocks.configuration.allow_message_expectations_on_nil` to `false`. Called from .../spec/whatever.rb:10:in `block (3 levels) in <top (required)>'`.
So specify CreatePayment
in the call to describe
and it should work.
Upvotes: 0