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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With