Wie man in RSpec beliebig oft "any_instance" "should_receive" sagt

73

Ich habe einen Import-Controller in Rails, der mehrere CSV-Dateien mit mehreren Datensätzen in meine Datenbank importiert. Ich möchte in RSpec testen, ob die Datensätze tatsächlich mithilfe von RSpec gespeichert werden:

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

Ich erhalte jedoch die Fehlermeldung:

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

Ein erfundenes Beispiel für die Steuerung:

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

Ist es möglich, dies mit RSpec zu testen, oder gibt es eine Problemumgehung?

Harm de Wit
quelle
Welche Version von RSpec verwenden Sie und welche Fehlermeldung wird angezeigt?
David Chelimsky
rspec (2.8.0) und die Nachricht lautet: Die Nachricht 'save' wurde von <Modellinstanz> empfangen, wurde aber bereits von <einer anderen Modellinstanz> empfangen
Harm de Wit
Das ist das erwartete Verhalten. Der Sinn von any_instance besteht darin, nicht wissen zu müssen, welche einzelne Instanz etwas erwartet, es jedoch auf eine Instanz zu beschränken.
David Chelimsky
8
Es ist das erwartete Verhalten - selbstverständlich - aber es ist nicht sehr nützlich, wenn Sie dies testen möchten. Und es scheint keine andere Methode zu geben, wie "many_instances", die die Einschränkung einer Instanz lockert.
Rob

Antworten:

46

Hierfür gibt es eine neue Syntax:

expect_any_instance_of(Model).to receive(:save).at_least(:once)
Muirbot
quelle
3
Die Designer von rspec raten von der Verwendung von ab expect_any_instance_of. Hier ist ein Link zum Dokument .
Tyler Collier
@ TylerCollier Ja, ich stimme vollkommen zu. Aus diesem Dokument: "Die Verwendung dieser Funktion ist häufig ein Designgeruch. Es kann sein, dass Ihr Test versucht, zu viel zu tun, oder dass das zu testende Objekt zu komplex ist."
Muirbot
20
Das funktioniert bei mir eigentlich nicht; gleicher Fehler wie bei der ursprünglichen Frage. mit RSpec 3.3.3.
Steakchaser
Dann klingt es wie ein legitimer Fehler. Es gibt zwei Instanzen, die die Nachricht empfangen.
Muirbot
50

Hier ist eine bessere Antwort, bei der vermieden wird, dass Folgendes überschrieben werden muss: Neue Methode:

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

Es scheint, dass die Stub-Methode ohne die Einschränkung an jede Instanz angehängt werden kann und der do-Block eine Zählung durchführen kann, die Sie überprüfen können, um sicherzustellen, dass sie die richtige Anzahl von Malen aufgerufen wurde.

Update - Neue rspec-Version erfordert diese 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
rauben
quelle
Ah schön mit stub auf any_instance! Sieht schöner und dynamischer aus als meine Lösung.
Harm de Wit
1
Obwohl Ihre weniger hackig ist, mag ich keine Zähler, da ich durch Proxys auf eine Klassenmethode [hier] [ stackoverflow.com/a/15038406/619510] dieselbe Syntax beibehalten habe.
Michelpm
Brillant! Nimm dafür +1. : D
nichts Besonderes-hier
15

Ich habe es endlich geschafft, einen Test zu machen, der für mich funktioniert:

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

Die Stub-Methode gibt eine einzelne Instanz zurück, die die Speichermethode mehrmals empfängt. Da es sich um eine einzelne Instanz handelt, kann ich die any_instanceMethode löschen und at_leastnormal verwenden.

Harm de Wit
quelle
11

Stub so

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

Dann erwarten Sie so:

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

Dies ist eine Vereinfachung dieses Kerns , any_instanceda Sie keinen Proxy für die ursprüngliche Methode benötigen. Beziehen Sie sich für andere Zwecke auf dieses Wesentliche.

michelpm
quelle
1
Ausgezeichnet. Das Proxying der Methode zu einer bekannten Instanz funktioniert hervorragend. Sie können genauso gut ein Testdouble anstelle der User-Klasse stubben (: speichern).
Matt Connolly
Klappt wunderbar! Vielen Dank! :-)
wrzasa
Verwenden Sie einfach allowund allow_any_instance_ofanstelle von stubund any_instance.stubda die ersteren jetzt veraltet sind (siehe: github.com/rspec/… )
wrzasa
11

Dies ist Robs Beispiel mit RSpec 3.3, das nicht mehr unterstützt wird Foo.any_instance. Ich fand dies nützlich, wenn in einer Schleife Objekte erstellt wurden

# 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
quelle
3

Mein Fall war ein bisschen anders, aber ich kam zu dieser Frage, um meine Antwort auch hier fallen zu lassen. In meinem Fall wollte ich jede Instanz einer bestimmten Klasse stubben. Ich habe den gleichen Fehler bekommen, als ich verwendet habe expect_any_instance_of(Model).to. Als ich es änderte, allow_any_instance_of(Model).towar mein Problem gelöst.

Weitere Hintergrundinformationen finden Sie in der Dokumentation: https://github.com/rspec/rspec-mocks#settings-mocks-or-stubs-on-any-instance-of-a-class

Rick Pastoor
quelle
Vielen Dank! Das ist die nützlichste Antwort für mich in diesem Thread.
Rafał Cieślak
Aus den Dokumenten: "Die rspec-mocks-API wurde für einzelne Objektinstanzen entwickelt, diese Funktion funktioniert jedoch für ganze Objektklassen. Infolgedessen gibt es einige semantisch verwirrende Randfälle. Beispielsweise expect_any_instance_of(Widget).to receive(:name).twiceist nicht klar, ob jede bestimmte Instanz vorhanden ist wird voraussichtlich namezweimal empfangen , oder wenn zwei insgesamt empfangen werden. (Es ist das erstere.) "
Rafał Cieślak
0

Sie können versuchen, die Anzahl der newin der Klasse zu zählen. Das testet eigentlich nicht die Anzahl der saves, kann aber ausreichen

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

Wenn es die einzige Erwartung gibt, wie oft es gespeichert wurde. Dann möchten Sie wahrscheinlich spy()anstelle der voll funktionsfähigen Fabrik verwenden, wie in der Harm de Witeigenen Antwort

    allow(Mutation).to receive(:new).and_return(spy)
    ...
    expect(Mutation.new).to have_received(:save).at_least(:once)
Brazhnyk Yuriy
quelle