Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

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

like image 911
Uzzar Avatar asked Dec 25 '22 00:12

Uzzar


1 Answers

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

like image 169
Laura Paakkinen Avatar answered Dec 30 '22 08:12

Laura Paakkinen