Carl
Carl

Reputation: 1276

Rspec Throws ArgumentError with or without arguments

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

Answers (3)

Frank Kwok
Frank Kwok

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

Cassandra S.
Cassandra S.

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

Paul
Paul

Reputation: 1167

Ok, there are a few things at play here, so let's go one by one.


Class/instance methods.

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

Ancestors

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.


Described_class

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

Related Questions