ruby on rails - Problemas para comparar el tiempo con RSpec
ruby-on-rails time (6)
El objeto Ruby Time mantiene una mayor precisión que la base de datos. Cuando el valor se lee desde la base de datos, solo se conserva a una precisión de microsegundos, mientras que la representación en memoria es precisa a nanosegundos.
Si no te importa la diferencia de milisegundos, podrías hacer un to_s / to_i en ambos lados de tu 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 this para obtener más información sobre por qué los tiempos son diferentes
Estoy usando Ruby on Rails 4 y la gema rspec-rails 2.14. Para mi objetivo, me gustaría comparar la hora actual con el updated_at
objeto updated_at
después de ejecutar una acción de controlador, pero estoy en problemas ya que la especificación no pasa. Es decir, dado el 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, aparece 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 hacer que la especificación pase?
Nota : Intenté también lo siguiente (tenga en cuenta la adición utc
):
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 ==)
La manera más fácil que encontré alrededor de este problema es crear un método de prueba de tiempo current_time
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
Me parece más elegante usar el be_within
rspec matcher predeterminado:
expect(@article.updated_at.utc).to be_within(1.second).of Time.now
Publicación anterior, pero espero que ayude a cualquiera que ingrese aquí para encontrar una solución. Creo que es más fácil y más 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)
patch :update
@article.reload
expect(@article.updated_at).to eq(freezed_time)
end
Esto asegura que la fecha almacenada es la correcta, sin hacer to_x
ni preocuparse por los decimales.
Puede convertir el objeto date / datetime / time en una cadena, ya que 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)
sí, como Oin
está sugiriendo be_within
matcher es la mejor práctica
... y tiene más uscases -> http://www.eq8.eu/blogs/27-rspec-be_within-matcher
Pero una forma más de cómo lidiar con esto es usar Rails incorporados en los atributos de midday
y middnight
.
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
¡Ahora esto es solo para demostración!
No usaría esto en un controlador ya que está anulando todo el tiempo. Nuevas llamadas => todos los atributos de tiempo tendrán el mismo tiempo => puede que no prueben el concepto que intentan alcanzar. Normalmente lo uso en objetos de Ruby compuestos similares a esto:
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, sinceramente, recomiendo seguir con be_within
matcher incluso comparando Time.now.midday.
Así que sí, los pls se quedan con be_within
matcher;)
actualización 2017-02
Pregunta en comentario:
¿Qué pasa si los tiempos están en un Hash? ¿Hay alguna forma de que funcione (hash_1) .to eq (hash_2) 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 matcher RSpec al match
coincidencia (por ejemplo, incluso puede hacer pruebas API con RSpec puro )
En cuanto a "post-db-times", supongo que te refieres a una cadena que se genera después de guardarla en DB. Yo sugeriría desvincular este caso a 2 expectativas (una asegurando la estructura de hash, la segunda revisando el tiempo). Así que puedes 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 es muy frecuente en su suite de pruebas, le sugiero que escriba su propio repetidor RSpec (por ejemplo, be_near_time_now_db_string
) convirtiendo el tiempo de serie de db en objeto Time y luego use esto como parte de la match(hash)
:
expect(hash).to match({mytime: be_near_time_now_db_string}) # you need to write your own matcher for this to work.