Problemi nel confrontare il tempo con RSpec


119

Sto usando Ruby su Rails 4 e rspec-rails gem 2.14. Per un oggetto my vorrei confrontare l'ora corrente con l' updated_atattributo object dopo l'esecuzione di un'azione del controller, ma sono nei guai poiché la specifica non viene superata. Cioè, dato quanto segue è il codice delle specifiche:

it "updates updated_at attribute" do
  Timecop.freeze

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

Quando eseguo la specifica sopra, ottengo il seguente errore:

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

Come posso far passare la specifica?


Nota : ho provato anche quanto segue (nota il fileutc aggiunta):

it "updates updated_at attribute" do
  Timecop.freeze

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

ma la specifica continua a non essere soddisfatta (nota la differenza di valore "ottenuto"):

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

Sta confrontando gli ID oggetto, quindi il testo di inspect corrisponde, ma sotto ci sono due diversi oggetti Time. Potresti semplicemente usare ===, ma potresti soffrire di attraversare i secondi confini. Probabilmente la cosa migliore è trovare o scrivere il proprio matcher, in cui si converte in epoche secondi e si tiene conto di una piccola differenza assoluta.
Neil Slater

Se ho capito che stai parlando di "attraversare i secondi confini", il problema non dovrebbe sorgere visto che sto usando la gemma Timecop che "congela" il tempo.
Backo

Ah mi è mancato, scusa. In tal caso, usa semplicemente ===invece di ==- attualmente stai confrontando object_id di due diversi oggetti Time. Anche se Timecop non bloccherà l'ora del server del database. . . quindi se i tuoi timestamp vengono generati dall'RDBMS non funzionerebbe (mi aspetto che non sia un problema per te qui però)
Neil Slater

Risposte:


156

L'oggetto Ruby Time mantiene una precisione maggiore rispetto al database. Quando il valore viene riletto dal database, viene conservato solo con una precisione al microsecondo, mentre la rappresentazione in memoria è precisa al nanosecondo.

Se non ti interessa la differenza di millisecondi, potresti fare un to_s / to_i su entrambi i lati delle tue aspettative

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

o

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

Fare riferimento a questo per ulteriori informazioni sul motivo per cui i tempi sono diversi


Come puoi vedere nel codice dalla domanda, io uso la Timecopgemma. Dovrebbe semplicemente risolvere il problema "congelando" il tempo?
Backo

Risparmi il tempo nel database e lo recuperi (@ article.updated_at), il che gli fa perdere i nanosecondi dove come Time.now ha mantenuto il nanosecondo. Dovrebbe essere chiaro dalle prime righe della mia risposta
usha

3
La risposta accettata è di quasi un anno più vecchia della risposta qui sotto - il matcher be_within è un modo molto migliore di gestirlo: A) non hai bisogno di una gemma; B) funziona per qualsiasi tipo di valore (intero, float, data, ora, ecc.); C) è nativamente parte di RSpec
notaceo

3
Questa è una soluzione "OK", ma sicuramente be_withinè quella giusta
Francesco Belladonna

Ciao, sto usando confronti datetime con aspettative di ritardo di accodamento lavoro expect {FooJob.perform_now}.to have_enqueued_job(FooJob).at(some_time) Non sono sicuro che il .atmatcher accetterebbe un tempo convertito in intero con .to_iqualcuno che ha affrontato questo problema?
Cyril Duchon-Doris,

200

Trovo be_withinpiù elegante l'utilizzo del matcher rspec predefinito:

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

2
.000001 s è un po 'stretto. Ho provato anche .001 ea volte falliva. Anche 0,1 secondi credo che dimostri che il tempo è impostato su adesso. Abbastanza buono per i miei scopi.
smoyth

3
Penso che questa risposta sia migliore perché esprime l'intento del test, piuttosto che oscurarlo dietro metodi non correlati come to_s. Inoltre, to_ie to_spotrebbe non funzionare di rado se il tempo è prossimo alla fine di un secondo.
B Seven

2
Sembra che il be_withinmatcher sia stato aggiunto a RSpec 2.1.0 il 7 novembre 2010 e migliorato alcune volte da allora. rspec.info/documentation/2.14/rspec-expectations/…
Mark Berry

1
Capisco undefined method 'second' for 1:Fixnum. C'è qualcosa di cui ho bisogno require?
David Moles

3
@DavidMoles .secondè un'estensione di rails: api.rubyonrails.org/classes/Numeric.html
jwadsack

10

sì, come Oinsuggerisce che il be_withinmatcher sia la migliore pratica

... e ha altre uscases -> http://www.eq8.eu/blogs/27-rspec-be_within-matcher

Ma un altro modo per affrontare questo problema è usare Rails integrato middaye middnightattributi.

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

Ora questo è solo per dimostrazione!

Non lo userei in un controller perché stai bloccando tutte le chiamate Time.new => tutti gli attributi temporali avranno lo stesso tempo => potrebbe non dimostrare il concetto che stai cercando di ottenere. Di solito lo uso in oggetti Ruby composti simili a questo:

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

Ma onestamente consiglio di restare con be_withinMatcher anche quando confronti Time.now.midday!

Quindi sì, ti prego di attaccare con be_withinmatcher;)


aggiornamento 2017-02

Domanda nel commento:

cosa succede se i tempi sono in un hash? qualche modo per far funzionare l'aspettativa (hash_1) .to eq (hash_2) quando alcuni valori hash_1 sono pre-db-time ei valori corrispondenti in hash_2 sono post-db-time? -

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

puoi passare qualsiasi matcher RSpec al matchmatcher (così ad esempio puoi anche eseguire test API con RSpec puro )

Per quanto riguarda "post-db-times", immagino che tu intenda una stringa generata dopo il salvataggio su DB. Suggerirei di disaccoppiare questo caso a 2 aspettative (una che garantisce la struttura hash, la seconda controlla l'ora) Quindi puoi fare qualcosa come:

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)

Ma se questo caso è troppo spesso nella tua suite di test, suggerirei di scrivere il tuo RSpec matcher (ad esempio be_near_time_now_db_string) convertendo il tempo della stringa db in oggetto Time e quindi usalo come parte di match(hash):

 expect(hash).to match({mytime: be_near_time_now_db_string})  # you need to write your own matcher for this to work.

cosa succede se i tempi sono in un hash? qualche modo per expect(hash_1).to eq(hash_2)funzionare quando alcuni valori hash_1 sono pre-db-time ei valori corrispondenti in hash_2 sono post-db-times?
Michael Johnston,

Stavo pensando a un hash arbitrario (la mia specifica afferma che un'operazione non cambia affatto un record). Ma grazie!
Michael Johnston

10

Vecchio post, ma spero che aiuti chiunque entri qui per una soluzione. Penso che sia più facile e più affidabile creare la data manualmente:

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

Ciò garantisce che la data memorizzata sia quella giusta, senza fare to_xo preoccuparsi dei decimali.


Assicurati di sbloccare il tempo alla fine delle specifiche
Qwertie

L'ho cambiato per usare invece un blocco. In questo modo non puoi dimenticare di tornare indietro al tempo normale.
jBilbo

8

È possibile convertire l'oggetto data / datetime / ora in una stringa poiché è archiviato nel database con 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)

7

Il modo più semplice che ho trovato per aggirare questo problema è creare un current_timemetodo helper di test in questo modo:

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

Ora il tempo è sempre arrotondato al millisecondo più vicino ai confronti sono semplici:

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

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

1
.change(usec: 0)è molto utile
Koen.

2
Possiamo usare questo .change(usec: 0)trucco per risolvere il problema senza usare anche un assistente per le specifiche. Se la prima riga è, Timecop.freeze(Time.current.change(usec: 0))allora possiamo semplicemente confrontare .to eq(Time.now)alla fine.
Harry Wood,

0

Poiché stavo confrontando gli hash, la maggior parte di queste soluzioni non funzionava per me, quindi ho scoperto che la soluzione più semplice era semplicemente prendere i dati dall'hash che stavo confrontando. Poiché i tempi updated_at non sono effettivamente utili per me per testare questo funziona bene.

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

expect(data).to eq(
  {updated_at: data[:updated_at], some_other_keys: ...}
)
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.