it-roy-ru.com

Трудно сравнивать время с RSpec

Я использую Ruby на Rails 4 и гем rspec-Rails 2.14. Для объекта my я хотел бы сравнить текущее время с атрибутом объекта 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 ==)
107
Backo

Объект 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)

Обратитесь к это для получения дополнительной информации о том, почему времена разные

137
usha

Я считаю использование be_within по умолчанию для сопоставления rspec более элегантным:

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

Старый пост, но я надеюсь, что он поможет любому, кто придет сюда для решения. Я думаю, что проще и надежнее просто создать дату вручную:

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 и не заботясь о десятичных числах.

8
jBilbo

да, поскольку Oin предлагает be_within matcher - лучшая практика

... и у него есть еще несколько случаев -> 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 => все атрибуты времени будут иметь одно и то же время =>, возможно, не докажут концепцию, которую вы пытаетесь достичь. Я обычно использую его в составных 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 matcher даже при сравнении Time.now.midday!

Так что да, пожалуйста, придерживайтесь 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", я предполагаю, что вы имеете в виду строку, которая генерируется после сохранения в БД. Я бы предложил разделить этот случай на 2 ожидания (одно гарантирует структуру хеша, второе проверяет время), так что вы можете сделать что-то вроде:

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 matcher (например, be_near_time_now_db_string) конвертирующий время строки db в объект 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.
8
equivalent8

Вы можете преобразовать объект 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)
7
Thomas Klemm

Самый простой способ, который я нашел для решения этой проблемы, - создать вспомогательный метод тестирования 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
6
Sam Davies