Probleme beim Vergleichen der Zeit mit RSpec

118

Ich benutze Ruby on Rails 4 und das rspec-Rails Gem 2.14. Für ein mein Objekt möchte ich die aktuelle Zeit mit dem updated_atObjektattribut nach einem Controller-Aktionslauf vergleichen, aber ich habe Probleme, da die Spezifikation nicht bestanden wird. Das heißt, wenn der folgende Spezifikationscode angegeben ist:

it "updates updated_at attribute" do
  Timecop.freeze

  patch :update
  @article.reload
  expect(@article.updated_at).to eq(Time.now)
end

Wenn ich die obige Spezifikation ausführe, wird folgende Fehlermeldung angezeigt:

Failure/Error: expect(@article.updated_at).to eq(Time.now)

   expected: 2013-12-05 14:42:20 UTC
        got: Thu, 05 Dec 2013 08:42:20 CST -06:00

   (compared using ==)

Wie kann ich die Spezifikation bestehen lassen?


Hinweis : Ich habe auch Folgendes versucht (beachten Sie den utcZusatz):

it "updates updated_at attribute" do
  Timecop.freeze

  patch :update
  @article.reload
  expect(@article.updated_at.utc).to eq(Time.now)
end

Die Spezifikation wird jedoch immer noch nicht bestanden (beachten Sie die "got" -Wertdifferenz):

Failure/Error: expect(@article.updated_at.utc).to eq(Time.now)

   expected: 2013-12-05 14:42:20 UTC
        got: 2013-12-05 14:42:20 UTC

   (compared using ==)
Backo
quelle
Es werden die Objekt-IDs verglichen, daher stimmt der Text von inspect überein, aber darunter befinden sich zwei verschiedene Zeitobjekte. Sie könnten nur verwenden ===, aber das kann unter dem Überschreiten der zweiten Grenzen leiden. Wahrscheinlich ist es am besten, einen eigenen Matcher zu finden oder zu schreiben, in dem Sie in Epochensekunden konvertieren und einen kleinen absoluten Unterschied berücksichtigen.
Neil Slater
Wenn ich verstanden habe, dass Sie "zweite Grenzen überschreiten", sollte das Problem nicht auftreten, da ich den Timecop- Edelstein verwende, der die Zeit "einfriert".
Backo
Ah, das habe ich verpasst, sorry. In diesem Fall verwenden Sie einfach ===anstelle von ==- Derzeit vergleichen Sie die object_id von zwei verschiedenen Zeitobjekten. Obwohl Timecop die Datenbankserverzeit nicht einfriert. . . Wenn Ihre Zeitstempel vom RDBMS generiert werden, funktioniert dies nicht (ich gehe davon aus, dass dies hier kein Problem für Sie ist)
Neil Slater,

Antworten:

155

Das Ruby Time-Objekt ist präziser als die Datenbank. Wenn der Wert aus der Datenbank zurückgelesen wird, bleibt er nur mit einer Genauigkeit von Mikrosekunden erhalten, während die Darstellung im Speicher auf Nanosekunden genau ist.

Wenn Ihnen der Millisekundenunterschied egal ist, können Sie auf beiden Seiten Ihrer Erwartung ein to_s / to_i ausführen

expect(@article.updated_at.utc.to_s).to eq(Time.now.to_s)

oder

expect(@article.updated_at.utc.to_i).to eq(Time.now.to_i)

Siehe diese für weitere Informationen darüber , warum die Zeiten sind anders

usha
quelle
Wie Sie im Code der Frage sehen können, verwende ich den TimecopEdelstein. Sollte es das Problem nur lösen, indem es die Zeit "einfriert"?
Backo
Sie speichern die Zeit in der Datenbank und rufen sie ab (@ article.updated_at), wodurch die Nanosekunden verloren gehen, während Time.now die Nanosekunde beibehalten hat. Es sollte aus den ersten Zeilen meiner Antwort
klar sein
3
Die akzeptierte Antwort ist fast ein Jahr älter als die unten stehende Antwort - der be_within Matcher ist eine viel bessere Möglichkeit, damit umzugehen: A) Sie brauchen keinen Edelstein; B) es funktioniert für jede Art von Wert (Ganzzahl, Gleitkomma, Datum, Uhrzeit usw.); C) es ist nativ Teil von RSpec
notaceo
3
Dies ist eine "OK" -Lösung, aber definitiv be_withindie richtige
Francesco Belladonna
Hallo, ich verwende Datetime-Vergleiche mit den Erwartungen für die Verzögerung der Job-Warteschlange. expect {FooJob.perform_now}.to have_enqueued_job(FooJob).at(some_time) Ich bin nicht sicher, ob der .atMatcher eine in Integer konvertierte Zeit akzeptieren würde, wenn .to_ijemand mit diesem Problem konfrontiert ist.
Cyril Duchon-Doris
198

Ich finde die Verwendung des be_withinStandard-Rspec-Matchers eleganter:

expect(@article.updated_at.utc).to be_within(1.second).of Time.now
Oin
quelle
2
.000001 s ist etwas eng. Ich habe sogar .001 ausprobiert und es ist manchmal fehlgeschlagen. Selbst 0,1 Sekunden beweisen meiner Meinung nach, dass die Zeit auf jetzt eingestellt ist. Gut genug für meine Zwecke.
Smoyth
3
Ich denke, diese Antwort ist besser, weil sie die Absicht des Tests ausdrückt, anstatt sie hinter einigen nicht verwandten Methoden wie zu verschleiern to_s. Auch to_iund to_skann selten fehlschlagen, wenn die Zeit nahe dem Ende einer Sekunde ist.
B Sieben
2
Es sieht so aus, als ob der be_withinMatcher am 7. November 2010 zu RSpec 2.1.0 hinzugefügt und seitdem einige Male verbessert wurde. rspec.info/documentation/2.14/rspec-expectations/…
Mark Berry
1
Ich verstehe undefined method 'second' for 1:Fixnum. Muss ich etwas tun require?
David Moles
3
@ DavidMoles .secondist eine Rails-Erweiterung: api.rubyonrails.org/classes/Numeric.html
jwadsack
10

Ja, wie Oines nahelegt, ist be_withinMatcher die beste Vorgehensweise

... und es gibt noch einige weitere Fälle -> http://www.eq8.eu/blogs/27-rspec-be_within-matcher

Eine weitere Möglichkeit, damit umzugehen, besteht darin, integrierte Rails middayund middnightAttribute zu verwenden.

it do
  # ...
  stubtime = Time.now.midday
  expect(Time).to receive(:now).and_return(stubtime)

  patch :update 
  expect(@article.reload.updated_at).to eq(stubtime)
  # ...
end

Dies ist nur zur Demonstration!

Ich würde dies nicht in einem Controller verwenden, da Sie alle Time.new-Aufrufe stubben => Alle Zeitattribute haben dieselbe Zeit => beweisen möglicherweise nicht das Konzept, das Sie erreichen möchten. Normalerweise verwende ich es in zusammengesetzten Ruby-Objekten, die folgendermaßen aussehen:

class MyService
  attr_reader :time_evaluator, resource

  def initialize(resource:, time_evaluator: ->{Time.now})
    @time_evaluator = time_evaluator
    @resource = resource
  end

  def call
    # do some complex logic
    resource.published_at = time_evaluator.call
  end
end

require 'rspec'
require 'active_support/time'
require 'ostruct'

RSpec.describe MyService do
  let(:service) { described_class.new(resource: resource, time_evaluator: -> { Time.now.midday } ) }
  let(:resource) { OpenStruct.new }

  it do
    service.call
    expect(resource.published_at).to eq(Time.now.midday)    
  end
end

Aber ehrlich gesagt empfehle ich, be_withinauch beim Vergleich von Time.now.midday bei Matcher zu bleiben!

Also ja bitte bleib beim be_withinMatcher;)


Update 2017-02

Frage im Kommentar:

Was ist, wenn die Zeiten in einem Hash sind? Gibt es eine Möglichkeit, zu erwarten, dass (hash_1) .to eq (hash_2) funktioniert, wenn einige hash_1-Werte Pre-Db-Zeiten und die entsprechenden Werte in Hash_2 Post-Db-Zeiten sind? - -

expect({mytime: Time.now}).to match({mytime: be_within(3.seconds).of(Time.now)}) `

Sie können jeden RSpec-Matcher an den matchMatcher übergeben (z. B. können Sie sogar API-Tests mit reinem RSpec durchführen ).

Was "post-db-times" betrifft, meinen Sie wohl eine Zeichenfolge, die nach dem Speichern in der Datenbank generiert wird. Ich würde vorschlagen, diesen Fall an zwei Erwartungen zu entkoppeln (eine stellt die Hash-Struktur sicher, die zweite überprüft die Zeit). Sie können also Folgendes tun:

hash = {mytime: Time.now.to_s(:db)}
expect(hash).to match({mytime: be_kind_of(String))
expect(Time.parse(hash.fetch(:mytime))).to be_within(3.seconds).of(Time.now)

Wenn dieser Fall jedoch in Ihrer Testsuite zu häufig vorkommt, würde ich empfehlen, einen eigenen RSpec-Matcher zu schreiben (z. B. be_near_time_now_db_string), der die Zeit der Datenbankzeichenfolge in ein Time-Objekt konvertiert, und diesen dann als Teil des folgenden Elements zu verwenden match(hash):

 expect(hash).to match({mytime: be_near_time_now_db_string})  # you need to write your own matcher for this to work.
Äquivalent8
quelle
Was ist, wenn die Zeiten in einem Hash sind? expect(hash_1).to eq(hash_2)Gibt es eine Möglichkeit, Arbeit zu leisten , wenn einige Hash_1-Werte Pre-DB-Zeiten und die entsprechenden Werte in Hash_2 Post-DB-Zeiten sind?
Michael Johnston
Ich dachte an einen beliebigen Hash (meine Spezifikation besagt, dass eine Operation einen Datensatz überhaupt nicht ändert). Aber danke!
Michael Johnston
10

Alter Beitrag, aber ich hoffe, er hilft jedem, der hier eintritt, nach einer Lösung. Ich denke, es ist einfacher und zuverlässiger, das Datum einfach manuell zu erstellen:

it "updates updated_at attribute" do
  freezed_time = Time.utc(2015, 1, 1, 12, 0, 0) #Put here any time you want
  Timecop.freeze(freezed_time) do
    patch :update
    @article.reload
    expect(@article.updated_at).to eq(freezed_time)
  end
end

Dies stellt sicher, dass das gespeicherte Datum das richtige ist, ohne dass Sie to_xsich um Dezimalstellen kümmern müssen.

jBilbo
quelle
Stellen Sie sicher, dass Sie die Zeit am Ende der Spezifikation
auftauen
Es wurde geändert, um stattdessen einen Block zu verwenden. Auf diese Weise können Sie nicht vergessen, zur normalen Zeit zurückzukehren.
jBilbo
8

Sie können das Objekt Datum / Datum / Uhrzeit / Uhrzeit in eine Zeichenfolge konvertieren, wenn es in der Datenbank mit gespeichert ist to_s(:db).

expect(@article.updated_at.to_s(:db)).to eq '2015-01-01 00:00:00'
expect(@article.updated_at.to_s(:db)).to eq Time.current.to_s(:db)
Thomas Klemm
quelle
7

Der einfachste Weg, um dieses Problem zu umgehen, besteht darin, eine current_timeTesthilfemethode wie folgt zu erstellen :

module SpecHelpers
  # Database time rounds to the nearest millisecond, so for comparison its
  # easiest to use this method instead
  def current_time
    Time.zone.now.change(usec: 0)
  end
end

RSpec.configure do |config|
  config.include SpecHelpers
end

Jetzt wird die Zeit immer auf die nächste Millisekunde gerundet, um Vergleiche zu vereinfachen:

it "updates updated_at attribute" do
  Timecop.freeze(current_time)

  patch :update
  @article.reload
  expect(@article.updated_at).to eq(current_time)
end
Sam
quelle
1
.change(usec: 0)ist sehr hilfreich
Koen.
2
Wir können diesen .change(usec: 0)Trick verwenden, um das Problem zu lösen, ohne auch einen Spezifikationshelfer zu verwenden. Wenn die erste Zeile ist Timecop.freeze(Time.current.change(usec: 0)), können wir einfach .to eq(Time.now)am Ende vergleichen .
Harry Wood
0

Da ich Hashes verglichen habe, haben die meisten dieser Lösungen bei mir nicht funktioniert. Daher war es für mich am einfachsten, einfach die Daten aus dem Hash zu holen, den ich verglichen habe. Da die aktualisierten_at-Zeiten für mich zum Testen nicht wirklich nützlich sind, funktioniert dies einwandfrei.

data = { updated_at: Date.new(2019, 1, 1,), some_other_keys: ...}

expect(data).to eq(
  {updated_at: data[:updated_at], some_other_keys: ...}
)
Qwertie
quelle