Проблема порівняння часу з 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.