web-dev-qa-db-de.com

Probleme beim Vergleichen der Zeit mit RSpec

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 ==)
110
Backo

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

142
usha

Ich finde mit dem be_within standard rspec matcher eleganter:

expect(@article.updated_at.utc).to be_within(1.second).of Time.now
179
Oin

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.

8
jBilbo

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.
8
equivalent8

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)
7
Thomas Klemm

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
6
Sam Davies