Problemas para comparar el tiempo con RSpec

119

Estoy usando Ruby on Rails 4 y la gema rspec-rails 2.14. Para mi objeto, me gustaría comparar la hora actual con el updated_atatributo del objeto después de ejecutar una acción del controlador, pero estoy en problemas porque la especificación no pasa. Es decir, dado lo siguiente es el código de especificación:

it "updates updated_at attribute" do
  Timecop.freeze

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

Cuando ejecuto la especificación anterior, obtengo el siguiente error:

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

¿Cómo puedo aprobar la especificación?


Nota : probé también lo siguiente (tenga en cuenta la utcadición):

it "updates updated_at attribute" do
  Timecop.freeze

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

pero la especificación aún no pasa (tenga en cuenta la diferencia de valor "obtenido"):

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
fuente
Está comparando los identificadores de objeto, por lo tanto, el texto de inspeccionar coincide, pero debajo tiene dos objetos Time diferentes. Podrías usarlo ===, pero eso puede sufrir al cruzar segundos límites. Probablemente lo mejor sea encontrar o escribir su propio comparador, en el que convierta a segundos de época y permita una pequeña diferencia absoluta.
Neil Slater
Si entendí que te refieres a "cruzar segundas fronteras", el problema no debería surgir ya que estoy usando la gema Timecop que "congela" el tiempo.
Backo
Ah, me perdí eso, lo siento. En cuyo caso, simplemente use en ===lugar de ==- actualmente está comparando el object_id de dos objetos Time diferentes. Aunque Timecop no congelará el tiempo del servidor de la base de datos. . . así que si sus marcas de tiempo las genera el RDBMS, no funcionaría (aunque espero que no sea un problema para usted)
Neil Slater

Respuestas:

156

El objeto Ruby Time mantiene una mayor precisión que la base de datos. Cuando se vuelve a leer el valor de la base de datos, solo se conserva con una precisión de microsegundos, mientras que la representación en memoria es precisa en nanosegundos.

Si no le importa la diferencia de milisegundos, puede hacer un to_s / to_i en ambos lados de su expectativa

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)

Consulte esto para obtener más información sobre por qué los tiempos son diferentes.

usha
fuente
Como puede ver en el código de la pregunta, uso la Timecopgema. ¿Debería resolver el problema simplemente "congelando" el tiempo?
Backo
Ahorras el tiempo en la base de datos y lo recuperas (@ article.updated_at) lo que hace que pierda los nanosegundos donde Time.now retiene el nanosegundo. Debe quedar claro desde las primeras líneas de mi respuesta
usha
3
La respuesta aceptada es casi un año más antigua que la respuesta a continuación: be_within matcher es una forma mucho mejor de manejar esto: A) no necesitas una gema; B) funciona para cualquier tipo de valor (entero, flotante, fecha, hora, etc.); C) es parte nativa de RSpec
notaceo
3
Esta es una solución "OK", pero definitivamente be_withines la correcta
Francesco Belladonna
Hola, estoy usando comparaciones de fecha y hora con expectativas de retraso en expect {FooJob.perform_now}.to have_enqueued_job(FooJob).at(some_time) la .atpuesta en cola de trabajos. No estoy seguro de que el comparador acepte un tiempo convertido en entero con .to_ialguien que haya enfrentado este problema.
Cyril Duchon-Doris
200

Encuentro usar el be_withinmatcher rspec predeterminado más elegante:

expect(@article.updated_at.utc).to be_within(1.second).of Time.now
Oin
fuente
2
.000001 s es un poco ajustado. Intenté incluso .001 y algunas veces fallaba. Incluso .1 segundos, creo, demuestra que la hora se ha fijado ahora. Lo suficientemente bueno para mis propósitos.
Smoyth
3
Creo que esta respuesta es mejor porque expresa la intención de la prueba, en lugar de ocultarla detrás de algunos métodos no relacionados como to_s. También, to_iy to_spuede fallar con poca frecuencia si el tiempo está cerca del final de un segundo.
B Siete
2
Parece que el be_withincomparador se agregó a RSpec 2.1.0 el 7 de noviembre de 2010 y se mejoró varias veces desde entonces. rspec.info/documentation/2.14/rspec-expectations/…
Mark Berry
1
Yo entiendo undefined method 'second' for 1:Fixnum. ¿Hay algo que necesite require?
David Moles
3
@DavidMoles .secondes una extensión de rieles: api.rubyonrails.org/classes/Numeric.html
jwadsack
10

sí, como Oinsugiere que el be_withinmatcher es la mejor práctica

... y tiene algunos uscases más -> http://www.eq8.eu/blogs/27-rspec-be_within-matcher

Pero una forma más de cómo lidiar con esto es usar Rails integrados middayy middnightatributos.

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

¡Esto es solo para demostración!

No usaría esto en un controlador, ya que está eliminando todas las llamadas de Time.new => todos los atributos de tiempo tendrán el mismo tiempo => puede que no pruebe el concepto que está tratando de lograr. Normalmente lo uso en objetos de Ruby compuestos similares a este:

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

Pero honestamente, recomiendo seguir con el be_withinmatcher incluso al comparar Time.now.midday!

Así que sí, quédate con el be_withinmatcher;)


actualización 2017-02

Pregunta en comentario:

¿y si los tiempos están en un hash? ¿Alguna forma de hacer que wait (hash_1) .to eq (hash_2) funcione cuando algunos valores hash_1 son pre-db-times y los valores correspondientes en hash_2 son post-db-times? -

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

puede pasar cualquier comparador RSpec al matchcomparador (por ejemplo, puede incluso hacer pruebas de API con RSpec puro )

En cuanto a "post-db-times", supongo que te refieres a una cadena que se genera después de guardar en DB. Sugeriría desacoplar este caso a 2 expectativas (una asegurando la estructura hash, la segunda verificando el tiempo) Para que pueda hacer algo como:

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)

Pero si este caso se encuentra con demasiada frecuencia en su conjunto de pruebas, le sugiero que escriba su propio comparador RSpec (por ejemplo be_near_time_now_db_string) convirtiendo el tiempo de cadena db en un objeto Time y luego use esto como parte 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.
equivalente8
fuente
¿y si los tiempos están en un hash? ¿alguna forma de hacer que expect(hash_1).to eq(hash_2)funcione cuando algunos valores hash_1 son pre-db-times y los valores correspondientes en hash_2 son post-db-times?
Michael Johnston
Estaba pensando en un hash arbitrario (mi especificación afirma que una operación no cambia un registro en absoluto). ¡Pero gracias!
Michael Johnston
10

Publicación antigua, pero espero que ayude a cualquiera que entre aquí en busca de una solución. Creo que es más fácil y confiable crear la fecha 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

Esto asegura que la fecha almacenada sea la correcta, sin tener que to_xpreocuparse por los decimales.

jBilbo
fuente
Asegúrese de descongelar el tiempo al final de la especificación
Qwertie
Lo cambié para usar un bloque en su lugar. De esa forma no podrá olvidarse de volver a la hora normal.
jBilbo
8

Puede convertir el objeto date / datetime / time en una cadena tal como está almacenado en la base de datos 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)
Thomas Klemm
fuente
7

La forma más fácil que encontré para solucionar este problema es crear un current_timemétodo auxiliar de prueba como este:

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

Ahora, el tiempo siempre se redondea al milisegundo más cercano para que las comparaciones sean sencillas:

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

  patch :update
  @article.reload
  expect(@article.updated_at).to eq(current_time)
end
Sam
fuente
1
.change(usec: 0)es muy útil
Koen.
2
Podemos usar este .change(usec: 0)truco para resolver el problema sin usar también un asistente de especificaciones. Si la primera línea es Timecop.freeze(Time.current.change(usec: 0)), simplemente podemos comparar .to eq(Time.now)al final.
Harry Wood
0

Debido a que estaba comparando hashes, la mayoría de estas soluciones no me funcionaron, así que encontré que la solución más fácil era simplemente tomar los datos del hash que estaba comparando. Dado que updated_at veces no son realmente útiles para mí para probar, esto funciona bien.

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

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