Uzzar Uzzar - 1 month ago 13
Ruby Question

RSpec and Object Initialization in Ruby

Something tells me that I am missing a key concept/idea in testing or (the heavens forbid) how ruby initializes objects.

I have a class method that accepts two arguments and returns an instance of the said class. So it looks like this:

class Manager

def self.run(first_arg, second_arg)
new(first_arg, second_arg)
end
end


This is my RSpec test:

RSpec.describe Manager, type: :api do

let(:first_arg) { FactoryGirl.build_stubbed(:first_arg) }
let(:second_arg) { AccountMailer.new }

describe '#run' do
it "accepts two arguments" do
expect(Manager).to receive(:run).with(first_arg, second_arg)
Manager.run(first_arg, second_arg)
end

it "instantiates the class with 2 arguments" do
expect(Manager).to receive(:new).with(first_arg, second_arg)
Manager.run(first_arg, second_arg)
end
end
end


Being that (i believe) the method
:initialize
gets called by new, I updated the code to this:

class Manager
# add attr_reader for read access
attr_reader :first_arg, :second_arg

def initialize(first_arg, second_arg)
@first_arg = first_arg
@second_arg = second_arg
end

def self.run(first_arg, second_arg)
new(first_arg, second_arg)
end
end


My test fails and returns this error:

1) Manager#run instantiates the class
Failure/Error: expect(Manager).to receive(:new).with(first_arg, second_arg)
Wrong number of arguments. Expected 0, got 2.


My main question is this:

Why does it appear that the methods I am passing to initialize aren't being picked up in rspec? I expected the test to pass because
Manager.new
, given how
initialize
is defined in the class, will fail if not passed 2 arguments.

Also what does this line mean:

`Wrong number of arguments: Expected 0, got 2`?


Where is the method that is expecting 2 - the
#run
in the Manager class defined in my program? Or the
#run
in Manager class defined in RSpec test?

Can anyone please point out what I am missing here? Appreciate the feedback. Thank you.

Answer

I. I was able to recreate your issue with a code like this

class Manager
  def self.run(a, b)
    new(a, b)
  end
end

RSpec.configure do |config|
  config.mock_with :rspec do |mocks|
    mocks.verify_partial_doubles = true
  end
end

RSpec.describe Manager, type: :api do
  let(:a) { 1 }
  let(:b) { 2 }

  describe '#run' do
    it 'instantiates the class with 2 arguments' do
      expect(Manager).to receive(:new).with(a, b)
      Manager.run(a, b)
    end
  end
end

Which results in:

1) Manager#run instantiates the class with 2 arguments
   Failure/Error: expect(Manager).to receive(:new).with(a, b)
     Wrong number of arguments. Expected 0, got 2.

This happens because of the verifying functionality. When this setting is enabled (and it should be), RSpec will make sure the the object implements the interface that is being stubbed/mocked. In this case RSpec throws an error on the line expect(Manager).to receive(:new).with(a, b), because it actually looks into the Manager class and checks whether initialize can take 2 arguments.

If you change the manager to look like this the example will pass:

class Manager
  attr_reader :a, :b

  def initialize(a, b)
    @a = a
    @b = b
  end

  def self.run(a, b)
    new(a, b)
  end
end

II. But you don't really need to use mocks for functionality like this. If you are just checking whether the right kind of instance is returned it is better to just look at the real thing.

RSpec.describe Manager, type: :api do
  let(:a) { 1 }
  let(:b) { 2 }

  describe '#run' do
    subject { described_class.run(a, b) }

    it 'instantiates the class with 2 arguments' do
      expect(subject).to be_an_instance_of(Manager)
    end

    it 'sets a to the first argument' do
      expect(subject.a).to eq(a)
    end

    it 'sets b to the second argument' do
      expect(subject.b).to eq(b)
    end
  end
end

III. In this example:

expect(Manager).to receive(:run).with(first_arg, second_arg)
Manager.run(first_arg, second_arg)

You set up an assertion, and then immediately called the code to pass that assertion. So weren't really testing anything.

Mocking/stubbing correctly is fairly advanced testing concept, and it is easy to get it wrong so if you can go without it, just go without, it will make things easier.

If you want to learn more about what to test/when to mock. I recommend this talk by Sandi Metz. https://www.youtube.com/watch?v=URSWYvyc42M

Comments