Проблема порівняння часу з RSpec


119

Я використовую 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

але специфікація все ще не проходить (зверніть увагу на різницю "got"):

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

Порівнюється ідентифікатор об'єкта, отже, текст інспектування збігається, але під вами є два різні об'єкти часу. Можна просто скористатися ===, але це може страждати від перетину другої межі. Напевно, найкраще - знайти або написати власний матч, в якому ви перетворюєтесь на епохальні секунди і допускаєте невелику абсолютну різницю.
Ніл Слейтер

Якби я зрозумів, що ви пов’язані з "перетинанням другої межі", проблема не повинна виникати, оскільки я використовую дорогоцінний камінь Timecop, який "заморожує" час.
Бачко

Ах, я пропустив це, вибачте. У такому випадку просто використовуйте ===замість цього ==- зараз ви порівнюєте object_id двох різних об'єктів Time. Хоча Timecop не заморозить час сервера бази даних. . . тому, якщо ваші часові позначки створюються RDBMS, це не спрацює (я думаю, це для вас тут не проблема)
Ніл Слейтер

Відповіді:


156

Об'єкт 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)

Для отримання додаткової інформації про те, чому час відрізняється, зверніться до цього


Як ви бачите в коді з питання, я використовую Timecopдорогоцінний камінь. Чи варто просто вирішити питання, "заморозивши" час?
Бачко

Ви зберігаєте час у базі даних та отримуєте їх (@ article.update_at), що дозволяє втратити наносекунди там, де Time.now зберігає наносекунд. Це повинно бути зрозуміло з перших кількох рядків моєї відповіді
usha

3
Прийнята відповідь майже на рік старша за відповідь нижче - відповідність be_within набагато кращий спосіб вирішити це: А) вам не потрібен дорогоцінний камінь; Б) він працює для будь-якого типу значень (ціле число, плаваючий, дата, час тощо); C) це споконвічна частина RSpec
notaceo

3
Це рішення "ОК", але, безумовно, be_withinце правильне
Франческо Беладонна

Здрастуйте, я використовую порівняння дат із очікуванням затримки затримки в роботі, expect {FooJob.perform_now}.to have_enqueued_job(FooJob).at(some_time) я не впевнений, що .atвідповідач прийме час, перетворений на ціле число, з .to_iким-небудь стикався з цією проблемою?
Кирило Духон-Доріс

200

Я вважаю, що використання відповідника be_withinrspec за замовчуванням є більш елегантним:

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

2
.000001 s трохи тісний. Я спробував навіть .001, і це часом не вдавалось. Навіть .1 секунду, думаю, доводить, що час встановлюється зараз. Досить добре для моїх цілей.
smoyth

3
Я вважаю, що ця відповідь є кращою, оскільки вона виражає наміри тесту, а не приховує його за деякими непов'язаними методами, як-от to_s. Крім того , to_iі , to_sможливо , нерідко , якщо час ближче до кінця другого.
B Сім

2
Схоже, що be_withinматч був доданий до RSpec 2.1.0 7 листопада 2010 року та покращився в кілька разів. rspec.info/documentation/2.14/rspec-expeasures/…
Марк Беррі

1
Я отримую undefined method 'second' for 1:Fixnum. Щось мені потрібно require?
Девід Молес

3
@DavidMoles .second- це розширення для рейків
jwadsack

10

так, 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 => всі атрибути часу матимуть однаковий час => може не довести концепцію, яку ви намагаєтеся досягти. Зазвичай я використовую його в складених об'єктах 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!

Так що, будь ласка, дотримуйтесь be_withinматчера;)


оновлення 2017-02

Питання в коментарі:

що робити, якщо час у хеш? будь-який спосіб змусити очікувати (hash_1) .to eq (hash_2) працювати, коли деякі значення хеш_1 є до-db-разів, а відповідні значення в хеш -_2 - post-db-рази? -

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 (наприклад 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.

що робити, якщо час у хеш? будь-який спосіб зробити expect(hash_1).to eq(hash_2)роботу, коли деякі значення хеш_1 є до-дб-раз, а відповідні значення в хеш -_2 - після-дб-раз?
Майкл Джонстон

Я думав про довільний хеш (моя специфікація стверджує, що операція зовсім не змінює запис). Але дякую!
Майкл Джонстон

10

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

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

Це гарантує, що збережена дата є правильною, не роблячи to_xчи переживаючи десяткових знаків.


Переконайтеся, що ви розморожуєте час наприкінці специфікації
Qwertie

Змінив його, щоб замість цього використовувати блок. Таким чином, ви не можете забути про повернення до звичайного часу.
jBilbo

8

Ви можете перетворити об'єкт дата / дата / час у рядок, оскільки він зберігається в базі даних 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

Найпростіший спосіб, який я знайшов навколо цієї проблеми, - створити такий 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

1
.change(usec: 0)дуже допомагає
Коен.

2
Ми можемо використовувати цей .change(usec: 0)трюк, щоб вирішити проблему, не використовуючи також специфічного помічника. Якщо перший рядок, Timecop.freeze(Time.current.change(usec: 0))то ми можемо просто порівняти .to eq(Time.now)в кінці.
Гаррі Вуд

0

Оскільки я порівнював хеші, більшість цих рішень не працювали для мене, тому я знайшов, що найпростішим рішенням було просто схопити дані з хешу, який я порівнював. Оскільки час оновленого_ати фактично не корисний для тестування, це працює чудово.

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

expect(data).to eq(
  {updated_at: data[:updated_at], some_other_keys: ...}
)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.