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

그러나 사양은 여전히 ​​통과하지 못합니다 ( "값 차이가 있음"에 유의).

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

개체 ID를 비교하므로 inspect의 텍스트가 일치하지만 아래에는 두 개의 다른 Time 개체가 있습니다. 을 사용할 수 ===있지만 두 번째 경계를 넘으면 어려움을 겪을 수 있습니다. 아마도 가장 좋은 방법은 epoch 초로 변환하고 작은 절대 차이를 허용하는 자신 만의 matcher를 찾거나 작성하는 것입니다.
— Neil Slater

"두 번째 경계를 넘어서"라는 말을 이해했다면 시간을 "동결"시키는 Timecop gem을 사용하고 있기 때문에 문제가 발생하지 않아야합니다 .
— Backo dec. 05

아, 미안해. 어떤 경우에는 ===대신 사용하십시오 ==-현재 두 개의 다른 Time 객체의 object_id를 비교하고 있습니다. 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)

시간이 다른 이유에 대한 자세한 정보는 이것을 참조하십시오.


질문의 코드에서 볼 수 있듯이 나는 Timecopgem을 사용합니다 . 시간을 "고정"하여 문제를 해결해야합니까?
— Backo

데이터베이스에 시간을 절약하고 검색 (@ article.updated_at)하면 Time.now가 나노초를 유지하는 나노초가 느슨해집니다. 그것은 내 대답의 처음 몇 줄에서 명확해야
— USHA

3
허용되는 답변은 아래 답변보다 거의 1 년 더 오래되었습니다. be_within 매처는이를 처리하는 훨씬 더 좋은 방법입니다. A) 보석이 필요하지 않습니다. B) 모든 유형의 값 (정수, 부동, 날짜, 시간 등)에 대해 작동합니다. C) 기본적으로 RSpec의 일부입니다
— notaceo

3
이것은 "OK"해결책이지만, 확실히는 be_within바로 하나입니다
— 프란체스코 벨라

안녕하세요 저는 작업 대기열에 넣기 지연 예상과 함께 날짜 시간 비교를 사용하고 있습니다. expect {FooJob.perform_now}.to have_enqueued_job(FooJob).at(some_time) 하고 있습니다. 매 .at처가이 .to_i문제에 직면 한 사람 과 함께 정수로 변환 된 시간을 허용 할지 확신하지 못 합니까?
— Cyril Duchon-Doris

200

내가 사용 찾을 be_within기본 RSpec에 매처는 더 우아한 :

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 Seven

2
매 be_within처가 2010 년 11 월 7 일에 RSpec 2.1.0에 추가되었고 그 이후로 몇 번 개선 된 것 같습니다. rspec.info/documentation/2.14/rspec-expectations/...
— 마크 베리에게

1
나는 undefined method 'second' for 1:Fixnum. 내가 필요한 것이 require있습니까?
— David Moles

3
@DavidMoles .second는 레일 확장입니다 : api.rubyonrails.org/classes/Numeric.html
— jwadsack

10

네가 Oin제안하는대로be_within matcher가 모범 사례 합니다.

... 그리고 그것은 더 많은 uscases를 가지고 있습니다-> 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_withinTime.now.midday를 비교할 때에도 matcher 를 고수하는 것을 추천합니다 !

그래서 예 pls는 be_withinmatcher 와 함께 스틱 ;)


업데이트 2017-02

댓글에있는 질문 :

시간이 해시에 있으면 어떨까요? 일부 hash_1 값이 pre-db-times이고 hash_2의 해당 값이 post-db-time 일 때 expect (hash_1) .to eq (hash_2)가 작동하도록하는 방법이 있습니까? –

expect({mytime: Time.now}).to match({mytime: be_within(3.seconds).of(Time.now)}) `

RSpec matcher를 matcher에 전달할 수 있습니다 match(예 : 순수한 RSpec으로 API 테스트를 수행 할 수도 있습니다. ).

"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 문자열 시간을 변환 한 다음의 한 부분으로 이것을 사용합니다 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)일부 hash_1 값이 pre-db-time이고 hash_2의 해당 값이 post-db-time 일 때 작동 하는 방법이 있습니까?
— Michael Johnston

나는 임의의 해시를 생각하고 있었다 (내 사양은 작업이 레코드를 전혀 변경하지 않는다고 주장하고 있음). 하지만 감사합니다!
— Michael Johnston

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

날짜 / 날짜 / 시간 개체는 .NET을 사용하여 데이터베이스에 저장되므로 문자열로 변환 할 수 있습니다 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)매우 도움이됩니다
— Koen.

2
이 .change(usec: 0)트릭을 사용하여 사양 도우미를 사용하지 않고도 문제를 해결할 수 있습니다 . 첫 번째 줄이 있다면 끝에서 Timecop.freeze(Time.current.change(usec: 0))간단히 비교할 수 있습니다 .to eq(Time.now).
— Harry Wood

0

해시를 비교하고 있었기 때문에 이러한 솔루션의 대부분이 저에게 적합하지 않았기 때문에 가장 쉬운 솔루션은 비교중인 해시에서 데이터를 가져 오는 것입니다. updated_at 시간이 실제로 제대로 작동하는지 테스트하는 데 유용하지 않기 때문에.

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

expect(data).to eq(
  {updated_at: data[:updated_at], some_other_keys: ...}
)
당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.