Problème de comparaison du temps avec RSpec

119

J'utilise Ruby on Rails 4 et le gem 2.14 rspec-rails. Pour mon objet, je voudrais comparer l'heure actuelle avec l' updated_atattribut d'objet après l'exécution d'une action du contrôleur, mais je suis en difficulté car la spécification ne passe pas. Autrement dit, étant donné ce qui suit est le code de spécification:

it "updates updated_at attribute" do
  Timecop.freeze

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

Lorsque j'exécute les spécifications ci-dessus, j'obtiens l'erreur suivante:

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

Comment puis-je faire passer les spécifications?


Remarque : j'ai également essayé ce qui suit (notez l' utcajout):

it "updates updated_at attribute" do
  Timecop.freeze

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

mais la spécification ne passe toujours pas (notez la différence de valeur "obtenue"):

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
la source
Il compare les identifiants des objets, par conséquent, le texte de inspect correspond, mais en dessous, vous avez deux objets Time différents. Vous pouvez simplement utiliser ===, mais cela peut souffrir du franchissement des secondes limites. Le mieux est probablement de trouver ou d'écrire votre propre matcher, dans lequel vous convertissez en secondes d'époque et permettez une petite différence absolue.
Neil Slater
Si je vous ai bien compris "franchir les secondes limites", le problème ne devrait pas se poser puisque j'utilise la gemme Timecop qui "fige" l'heure.
Backo
Ah j'ai raté ça, désolé. Dans ce cas, utilisez simplement ===au lieu de ==- vous comparez actuellement object_id de deux objets Time différents. Bien que Timecop ne gèle pas l'heure du serveur de base de données. . . donc si vos horodatages sont générés par le SGBDR, cela ne fonctionnera pas (je suppose que ce n'est pas un problème pour vous ici cependant)
Neil Slater

Réponses:

156

L'objet Ruby Time maintient une plus grande précision que la base de données. Lorsque la valeur est lue à partir de la base de données, elle n'est conservée qu'à la microseconde, tandis que la représentation en mémoire est précise en nanosecondes.

Si vous ne vous souciez pas de la différence en millisecondes, vous pouvez faire un to_s / to_i des deux côtés de votre attente

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

ou

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

Reportez-vous à ceci pour plus d'informations sur les raisons pour lesquelles les heures sont différentes

usha
la source
Comme vous pouvez le voir dans le code de la question, j'utilise le Timecopgem. Doit-il simplement résoudre le problème en "figeant" l'heure?
Backo
Vous enregistrez le temps dans la base de données et le récupérez (@ article.updated_at) ce qui lui fait perdre les nanosecondes alors que Time.now a retenu la nanoseconde. Cela devrait être clair dès les premières lignes de ma réponse
usha
3
La réponse acceptée a presque un an de plus que la réponse ci-dessous - le be_within matcher est une bien meilleure façon de gérer cela: A) vous n'avez pas besoin d'un joyau; B) cela fonctionne pour tout type de valeur (entier, flottant, date, heure, etc.); C) il fait partie nativement de RSpec
notaceo
3
C'est une solution «OK», mais c'est certainement be_withinla bonne
Francesco Belladonna
Bonjour, j'utilise des comparaisons datetime avec les attentes de délai de mise en file d'attente des travaux. expect {FooJob.perform_now}.to have_enqueued_job(FooJob).at(some_time) Je ne suis pas sûr que le .atmatcher accepterait un temps converti en entier avec .to_iquelqu'un a rencontré ce problème?
Cyril Duchon-Doris
200

Je trouve que l'utilisation du be_withinmatcher rspec par défaut est plus élégante:

expect(@article.updated_at.utc).to be_within(1.second).of Time.now
Oin
la source
2
.000001 s est un peu serré. J'ai même essayé .001 et ça échouait parfois. Même 0,1 seconde, je pense, prouve que l'heure est réglée maintenant. Assez bien pour mes besoins.
smoyth
3
Je pense que cette réponse est meilleure parce qu'elle exprime l'intention du test, plutôt que de l'obscurcir derrière des méthodes indépendantes comme to_s. Aussi, to_iet to_speut échouer rarement si le temps est proche de la fin d'une seconde.
B Seven
2
On dirait que le be_withinmatcher a été ajouté à RSpec 2.1.0 le 7 novembre 2010, et amélioré plusieurs fois depuis. rspec.info/documentation/2.14/rspec-expectations/…
Mark Berry
1
Je reçois undefined method 'second' for 1:Fixnum. Y a-t-il quelque chose dont j'ai besoin require?
David Moles
3
@DavidMoles .secondest une extension de rails: api.rubyonrails.org/classes/Numeric.html
jwadsack
10

oui, car cela Oinsuggère que le be_withinmatcher est la meilleure pratique

... et il a quelques autres cas d'utilisation -> http://www.eq8.eu/blogs/27-rspec-be_within-matcher

Mais une autre façon de gérer cela est d'utiliser les Rails intégrés middayet les middnightattributs.

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

Maintenant, c'est juste pour la démonstration!

Je n'utiliserais pas cela dans un contrôleur car vous stubbing tous les appels Time.new => tous les attributs de temps auront le même temps => peuvent ne pas prouver le concept que vous essayez d'atteindre. Je l'utilise généralement dans des objets Ruby composés similaires à ceci:

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

Mais honnêtement, je recommande de rester avec be_withinmatcher même lorsque vous comparez Time.now.midday!

Alors oui, veuillez coller avec le be_withinmatcher;)


mise à jour 2017-02

Question en commentaire:

et si les temps sont dans un hachage? n'importe quel moyen de faire en sorte que expect (hash_1) .to eq (hash_2) fonctionne quand certaines valeurs de hash_1 sont pré-db-times et les valeurs correspondantes dans hash_2 sont post-db-times? -

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

vous pouvez passer n'importe quel matcher RSpec au matchmatcher (vous pouvez donc même faire des tests API avec RSpec pur )

En ce qui concerne "post-db-times", je suppose que vous voulez dire une chaîne qui est générée après l'enregistrement dans DB. Je suggérerais de découpler ce cas en 2 attentes (une assurant la structure de hachage, une deuxième vérification de l'heure) Vous pouvez donc faire quelque chose comme:

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)

Mais si ce cas est trop souvent dans votre suite de tests, je suggérerais d'écrire votre propre matcher RSpec (par exemple be_near_time_now_db_string) en convertissant le temps de la chaîne db en objet Time, puis de l'utiliser dans le cadre de 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
la source
et si les temps sont dans un hachage? un moyen de faire expect(hash_1).to eq(hash_2)fonctionner quand certaines valeurs de hash_1 sont pré-db-times et les valeurs correspondantes dans hash_2 sont post-db-times?
Michael Johnston
Je pensais à un hachage arbitraire (ma spécification affirme qu'une opération ne change pas du tout un enregistrement). Mais merci!
Michael Johnston
10

Ancien message, mais j'espère que cela aidera quiconque entre ici pour une solution. Je pense qu'il est plus facile et plus fiable de simplement créer la date manuellement:

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

Cela garantit que la date stockée est la bonne, sans faire to_xni se soucier des décimales.

jBilbo
la source
Assurez-vous de dégeler le temps à la fin de la spécification
Qwertie
Modifié pour utiliser un bloc à la place. De cette façon, vous ne pouvez pas oublier de revenir à l'heure normale.
jBilbo
8

Vous pouvez convertir l'objet date / datetime / time en une chaîne tel qu'il est stocké dans la base de données avec 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
la source
7

Le moyen le plus simple que j'ai trouvé autour de ce problème est de créer une current_timeméthode d'assistance de test comme ceci:

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

Désormais, l'heure est toujours arrondie à la milliseconde la plus proche pour que les comparaisons soient simples:

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

  patch :update
  @article.reload
  expect(@article.updated_at).to eq(current_time)
end
Sam
la source
1
.change(usec: 0)est très utile
Koen.
2
Nous pouvons utiliser cette .change(usec: 0)astuce pour résoudre le problème sans utiliser également un assistant de spécification. Si la première ligne est, Timecop.freeze(Time.current.change(usec: 0))nous pouvons simplement comparer .to eq(Time.now)à la fin.
Harry Wood
0

Parce que je comparais les hachages, la plupart de ces solutions ne fonctionnaient pas pour moi, alors j'ai trouvé que la solution la plus simple était simplement de récupérer les données du hachage que je comparais. Étant donné que les heures de mise à jour ne sont pas vraiment utiles pour moi, cela fonctionne correctement.

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

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