80

I've got an import controller in rails that imports several csv files with multiple records into my database. I would like to test in RSpec if the records are actually saved by using RSpec:

<Model>.any_instance.should_receive(:save).at_least(:once)

However i get the error saying:

The message 'save' was received by <model instance> but has already been received by <another model instance>

A contrived example of the controller:

rows = CSV.parse(uploaded_file.tempfile, col_sep: "|")

  ActiveRecord::Base.transaction do
    rows.each do |row| 
    mutation = Mutation.new
    row.each_with_index do |value, index| 
      Mutation.send("#{attribute_order[index]}=", value)
    end
  mutation.save          
end

Is it possible to test this using RSpec or is there any workaround?

Harm de Wit
  • 2,150
  • 2
  • 18
  • 24
  • What version of RSpec are you using and what's the failure message you're seeing? – David Chelimsky Mar 21 '12 at 12:54
  • rspec (2.8.0) and the message is: The message 'save' was received by but has already been received by – Harm de Wit Mar 21 '12 at 14:19
  • 1
    That's the expected behavior. The point of any_instance is to not have to know which single instance is expecting something, but it still constrains it to one instance. – David Chelimsky Mar 23 '12 at 01:23
  • 9
    It's the expected behaviour - granted- but it's not very useful if you want to test this. And there doesn't seem to be any other method, like "many_instances" that relaxes the constraint of one instance. – Rob Apr 03 '12 at 17:53

7 Answers7

55

Here's a better answer that avoids having to override the :new method:

save_count = 0
<Model>.any_instance.stub(:save) do |arg|
    # The evaluation context is the rspec group instance,
    # arg are the arguments to the function. I can't see a
    # way to get the actual <Model> instance :(
    save_count+=1
end
.... run the test here ...
save_count.should > 0

Seems that the stub method can be attached to any instance w/o the constraint, and the do block can make a count that you can check to assert it was called the right number of times.

Update - new rspec version requires this syntax:

save_count = 0
allow_any_instance_of(Model).to receive(:save) do |arg|
    # The evaluation context is the rspec group instance,
    # arg are the arguments to the function. I can't see a
    # way to get the actual <Model> instance :(
    save_count+=1
end
.... run the test here ...
save_count.should > 0
Rob
  • 4,404
  • 2
  • 32
  • 33
46

There's a new syntax for this:

expect_any_instance_of(Model).to receive(:save).at_least(:once)
Dennis Hackethal
  • 13,662
  • 12
  • 66
  • 115
muirbot
  • 2,061
  • 1
  • 20
  • 29
  • 4
    The designers of rspec discourage the use of `expect_any_instance_of`. Here's a [link to the doc](https://relishapp.com/rspec/rspec-mocks/docs/working-with-legacy-code/any-instance). – Tyler Collier Feb 20 '15 at 15:36
  • @TylerCollier yeah, I totally agree. From that doc: "Using this feature is often a design smell. It may be that your test is trying to do too much or that the object under test is too complex." – muirbot Feb 20 '15 at 20:25
  • 27
    This actually does not work for me; same error as original question. using RSpec 3.3.3. – steakchaser Dec 03 '15 at 18:37
  • Then it sounds like a legit failure. There are two instances that are receiving the message. – muirbot Dec 03 '15 at 21:42
16

This is Rob's example using RSpec 3.3, which no longer supports Foo.any_instance. I found this useful when in a loop creating objects

# code (simplified version)
array_of_hashes.each { |hash| Model.new(hash).write! }

# spec
it "calls write! for each instance of Model" do 
  call_count = 0
  allow_any_instance_of(Model).to receive(:write!) { call_count += 1 }

  response.process # run the test
  expect(call_count).to eq(2)
end
sp89
  • 408
  • 4
  • 9
15

I finally managed to make a test that works for me:

  mutation = FactoryGirl.build(:mutation)
  Mutation.stub(:new).and_return(mutation)
  mutation.should_receive(:save).at_least(:once)

The stub method returns one single instance that receives the save method multiple times. Because it is a single instance i can drop the any_instance method and use the at_least method normally.

Harm de Wit
  • 2,150
  • 2
  • 18
  • 24
11

Stub like this

User.stub(:save) # Could be any class method in any class
User.any_instance.stub(:save) { |*args| User.save(*args) }

Then expect like this:

# User.any_instance.should_receive(:save).at_least(:once)
User.should_receive(:save).at_least(:once)

This is a simplification of this gist, to use any_instance, since you don't need to proxy to the original method. Refer to that gist for other uses.

michelpm
  • 1,795
  • 2
  • 18
  • 31
  • 1
    Excellent. Proxying the method through to a known instance works great. You could just as easily stub(:save) on a test double instead of the User class. – Matt Connolly Apr 01 '14 at 00:08
  • Works like a charm! Thanks! :-) – wrzasa Dec 18 '15 at 23:32
  • Just use `allow` and `allow_any_instance_of` instead of `stub` and `any_instance.stub` since the former are now deprecated (see: https://github.com/rspec/rspec-mocks#settings-mocks-or-stubs-on-any-instance-of-a-class) – wrzasa Dec 18 '15 at 23:53
5

My case was a bit different, but I ended up at this question to figured to drop my answer here too. In my case I wanted to stub any instance of a given class. I got the same error when I used expect_any_instance_of(Model).to. When I changed it to allow_any_instance_of(Model).to, my problem was solved.

Check out the documentation for some more background: https://github.com/rspec/rspec-mocks#settings-mocks-or-stubs-on-any-instance-of-a-class

Rick Pastoor
  • 3,625
  • 1
  • 21
  • 24
  • Thank you! That's the most useful answer for me in this thread. – Rafał Cieślak Sep 15 '20 at 09:21
  • From the docs: "The rspec-mocks API is designed for individual object instances, but this feature operates on entire classes of objects. As a result there are some semantically confusing edge cases. For example in `expect_any_instance_of(Widget).to receive(:name).twice` it isn't clear whether each specific instance is expected to receive `name` twice, or if two receives total are expected. (It's the former.)" – Rafał Cieślak Sep 23 '20 at 14:17
0

You may try to count the number of new on the class. That is not actually tests the number of saves but may be enough

    expect(Mutation).to receive(:new).at_least(:once)

If there is the only expectation of how many times it was saved. Then you probably want to use spy() instead of fully functioning factory, as in Harm de Wit own answer

    allow(Mutation).to receive(:new).and_return(spy)
    ...
    expect(Mutation.new).to have_received(:save).at_least(:once)
Brazhnyk Yuriy
  • 464
  • 4
  • 10