Ich benutze Ruby on Rails 4 und das rspec-Rails gem 2.14. Für mein Objekt möchte ich die aktuelle Zeit mit dem updated_at
Objektattribut nach Ausführung einer Controller-Aktion, aber ich bin in Schwierigkeiten, da die Spezifikation nicht bestanden wird.
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 der folgende Fehler 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 zum Bestehen bringen?
Anmerkung: Ich habe auch folgendes ausprobiert (Anmerkung zum utc
-Zusatz):
it "updates updated_at attribute" do
Timecop.freeze
patch :update
@article.reload
expect(@article.updated_at.utc).to eq(Time.now)
end
aber die Spezifikation besteht immer noch nicht (beachte den "got" -Wertunterschied):
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 ==)
Das Ruby Time-Objekt behält eine höhere Genauigkeit bei als die Datenbank. Wenn der Wert aus der Datenbank zurückgelesen wird, bleibt er nur auf Mikrosekunden genau erhalten, während die speicherinterne Darstellung 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)
Unter this finden Sie weitere Informationen dazu, warum die Zeiten unterschiedlich sind
Ich finde mit dem be_within
standard rspec matcher eleganter:
expect(@article.updated_at.utc).to be_within(1.second).of Time.now
Alter Beitrag, aber ich hoffe es hilft jedem, der hier nach einer Lösung sucht. Ich denke, es ist einfacher und zuverlässiger, das Datum 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)
patch :update
@article.reload
expect(@article.updated_at).to eq(freezed_time)
end
Dies stellt sicher, dass das gespeicherte Datum das richtige ist, ohne to_x
oder sich Gedanken über Dezimalstellen machen.
ja, da Oin
vorschlägt, dass der be_within
- Matcher die beste Vorgehensweise ist
... und es gibt noch ein paar Uscases -> http://www.eq8.eu/blogs/27-rspec-be_within-matcher
Eine weitere Möglichkeit, damit umzugehen, besteht darin, die in den Attributen midday
und middnight
eingebauten Rails) 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 blockieren. => Alle Zeitattribute haben dieselbe Zeit. => Beweisen möglicherweise nicht, dass Sie versuchen, zu erreichen. Ich benutze es normalerweise in zusammengesetzten Ruby Objekten, die ungefähr so 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, beim Vergleichen von Time.now.midday beim Matcher be_within
Zu bleiben!
Also ja, bitte bleib bei be_within
;)
Update 2017-02
Frage im Kommentar:
was ist, wenn die Zeiten in einem Hash sind? Wie kann man erwarten, dass (hash_1) .to eq (hash_2) funktioniert, wenn einige hash_1-Werte vor und die entsprechenden Werte in hash_2 nach der db-Zeit liegen? -
expect({mytime: Time.now}).to match({mytime: be_within(3.seconds).of(Time.now)}) `
sie können jeden RSpec-Matcher an den match
-Matcher übergeben (zum Beispiel können Sie sogar API-Tests mit reinem RSpec )
Was "post-db-times" betrifft, so meinst du vermutlich einen String, der nach dem Speichern in DB generiert wird. Ich würde vorschlagen, diesen Fall mit 2 Erwartungen zu entkoppeln (eine stellt die Hash-Struktur sicher, die zweite überprüft die Zeit).
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)
Aber wenn dieser Fall in Ihrer Testsuite zu häufig vorkommt, würde ich vorschlagen, dass Sie Ihren eigenen RSpec-Matcher (z. B. be_near_time_now_db_string
) Schreiben, um die DB-Zeichenfolgenzeit in ein Time-Objekt zu konvertieren, und dies dann als Teil von verwenden das match(hash)
:
expect(hash).to match({mytime: be_near_time_now_db_string}) # you need to write your own matcher for this to work.
Mit to_s(:db)
können Sie das Datums-/Datums-/Uhrzeitobjekt in eine Zeichenfolge umwandeln, wenn es in der Datenbank gespeichert ist.
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)
Der einfachste Weg, um dieses Problem zu umgehen, ist das Erstellen eines current_time
Testhilfemethode wie folgt:
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