Время сравнения с RSpec
Я использую Ruby on Rails 4 и rspec-rails gem 2.14. Для моего объекта я хотел бы сравнить текущее время с атрибутом объекта updated_at
после запуска действия контроллера, но у меня проблемы, поскольку спецификация не проходит. То есть, учитывая следующий код спецификации:
it "updates updated_at attribute" do
Timecop.freeze
patch :update
@article.reload
expect(@article.updated_at).to eq(Time.now)
end
Когда я запускаю указанную выше спецификацию, я получаю следующую ошибку:
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 ==)
Как мне передать спецификацию?
Примечание. Я также попробовал следующее (обратите внимание на дополнение utc
):
it "updates updated_at attribute" do
Timecop.freeze
patch :update
@article.reload
expect(@article.updated_at.utc).to eq(Time.now)
end
но спецификация все еще не проходит (обратите внимание на разницу в значениях "получен" ):
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 ==)
Ответы
Ответ 1
Объект Ruby Time поддерживает большую точность, чем база данных. Когда значение считывается из базы данных, оно сохраняется только до микросекундной точности, тогда как представление в памяти точнее наносекунд.
Если вам не нужна разница в миллисекундах, вы можете сделать to_s/to_i с обеих сторон вашего ожидания
expect(@article.updated_at.utc.to_s).to eq(Time.now.to_s)
или
expect(@article.updated_at.utc.to_i).to eq(Time.now.to_i)
Обратитесь к this для получения дополнительной информации о том, почему разные времена
Ответ 2
Я нахожу использование be_within
сопоставления rspec по умолчанию be_within
более элегантным:
expect(@article.updated_at.utc).to be_within(1.second).of Time.now
Ответ 3
Старый пост, но я надеюсь, что это поможет всем, кто входит сюда для решения. Я думаю, что проще и надежнее просто создать дату вручную:
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
Это гарантирует, что сохраненная дата является правильной, не делая to_x
или беспокоясь о десятичных знаках.
Ответ 4
Вы можете преобразовать объект date/datetime/time в строку, хранящуюся в базе данных, с помощью 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)
Ответ 5
yep, поскольку Oin
предполагает, что be_within
совпадение - лучшая практика
... и у него есть еще несколько случаев → http://www.eq8.eu/blogs/27-rspec-be_within-matcher
Но еще один способ справиться с этим - использовать Rails, встроенные в атрибуты midday
и 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
Теперь это только для демонстрации!
Я бы не использовал это в контроллере, поскольку вы завершаете все Time.new calls = > все атрибуты времени будут иметь одинаковое время = > , возможно, не докажут, какую концепцию вы пытаетесь достичь. Я обычно использую его в составных Ruby-объектах, похожих на это:
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
Но честно рекомендую придерживаться be_within
совпадения даже при сравнении Time.now.midday!
Итак, да, pls придерживаются be_within
matcher;)
обновление 2017-02
Вопрос в комментарии:
что, если время в хэше? любой способ сделать ожидание (hash_1).to eq (hash_2) работать, когда некоторые значения hash_1 являются pre-db-times, а соответствующие значения в hash_2 являются post-db-times? -
expect({mytime: Time.now}).to match({mytime: be_within(3.seconds).of(Time.now)}) `
вы можете передать любой сопоставитель RSpec в подкаталог match
(например, вы даже можете выполнить тестирование API с помощью чистого RSpec)
Что касается "post-db-times", я думаю, вы имеете в виду строку, которая создается после сохранения в DB. Я бы предложил отделить этот случай от двух ожиданий (один из которых обеспечивал структуру хэша, второй - проверку времени). Таким образом, вы можете сделать что-то вроде:
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)
Но если этот случай слишком часто используется в вашем тестовом наборе, я бы предложил написать собственный сортировщик RSpec (например, be_near_time_now_db_string
), конвертирующий db string для объекта Time, а затем использовать его как часть match(hash)
:
expect(hash).to match({mytime: be_near_time_now_db_string}) # you need to write your own matcher for this to work.
Ответ 6
Самый простой способ найти эту проблему - создать тестовый вспомогательный метод current_time
, например:
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
Теперь время всегда округляется до ближайшей миллисекунды, чтобы сравнивать:
it "updates updated_at attribute" do
Timecop.freeze(current_time)
patch :update
@article.reload
expect(@article.updated_at).to eq(current_time)
end