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 내장 middaymiddnight속성 을 사용하는 것입니다.

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.