Sự cố khi so sánh thời gian với RSpec


119

Tôi đang sử dụng Ruby on Rails 4 và gem rspec-rails 2.14. Đối với đối tượng của tôi, tôi muốn so sánh thời gian hiện tại với updated_atthuộc tính đối tượng sau khi chạy hành động bộ điều khiển, nhưng tôi gặp rắc rối vì thông số kỹ thuật không vượt qua. Đó là, sau đây là mã đặc điểm:

it "updates updated_at attribute" do
  Timecop.freeze

  patch :update
  @article.reload
  expect(@article.updated_at).to eq(Time.now)
end

Khi tôi chạy thông số trên, tôi gặp lỗi sau:

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

Làm cách nào để vượt qua thông số kỹ thuật?


Lưu ý : Tôi cũng đã thử những cách sau (lưu ý phần utcbổ sung):

it "updates updated_at attribute" do
  Timecop.freeze

  patch :update
  @article.reload
  expect(@article.updated_at.utc).to eq(Time.now)
end

nhưng thông số kỹ thuật vẫn không vượt qua (lưu ý sự khác biệt giá trị "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 ==)

Nó đang so sánh các id đối tượng, do đó văn bản từ kiểm tra khớp với nhau, nhưng bên dưới bạn có hai đối tượng Thời gian khác nhau. Bạn chỉ có thể sử dụng ===, nhưng điều đó có thể bị vượt qua ranh giới thứ hai. Có lẽ tốt nhất là tìm hoặc viết trình so khớp của riêng bạn, trong đó bạn chuyển đổi thành giây kỷ nguyên và cho phép một sự khác biệt tuyệt đối nhỏ.
Neil Slater

Nếu tôi hiểu bạn liên quan đến việc "vượt qua ranh giới thứ hai", thì vấn đề sẽ không nảy sinh vì tôi đang sử dụng viên ngọc Timecop "đóng băng" thời gian.
Backo

Ah, tôi đã bỏ lỡ điều đó, xin lỗi. Trong trường hợp đó, chỉ sử dụng ===thay vì ==- hiện tại bạn đang so sánh object_id của hai đối tượng Thời gian khác nhau. Mặc dù Timecop sẽ không đóng băng thời gian máy chủ cơ sở dữ liệu. . . vì vậy nếu timestamps của bạn đang được tạo ra bởi các RDBMS nó sẽ không làm việc (Tôi hy vọng đó không phải là một vấn đề đối với bạn ở đây mặc dù)
Neil Slater

Câu trả lời:


156

Đối tượng Ruby Time duy trì độ chính xác cao hơn so với cơ sở dữ liệu. Khi giá trị được đọc lại từ cơ sở dữ liệu, nó chỉ được bảo toàn với độ chính xác đến micro giây, trong khi biểu diễn trong bộ nhớ chính xác đến nano giây.

Nếu bạn không quan tâm đến sự khác biệt mili giây, bạn có thể thực hiện to_s / to_i ở cả hai phía mà bạn mong đợi

expect(@article.updated_at.utc.to_s).to eq(Time.now.to_s)

hoặc là

expect(@article.updated_at.utc.to_i).to eq(Time.now.to_i)

Tham khảo phần này để biết thêm thông tin về lý do tại sao thời gian lại khác nhau


Như bạn có thể thấy trong đoạn mã từ câu hỏi, tôi sử dụng Timecopđá quý. Nó có nên chỉ giải quyết vấn đề bằng cách "đóng băng" thời gian?
Backo

Bạn tiết kiệm thời gian trong cơ sở dữ liệu và truy xuất nó (@ article.updated_at), điều này làm cho nó mất đi từng nano giây trong đó Time.now đã giữ lại nano giây. Nó phải rõ ràng từ vài dòng đầu tiên của câu trả lời của tôi
usha

3
Câu trả lời được chấp nhận cũ hơn gần một năm so với câu trả lời bên dưới - be_within matcher là cách tốt hơn nhiều để xử lý điều này: A) bạn không cần đá quý; B) nó hoạt động với bất kỳ loại giá trị nào (số nguyên, số thực, ngày, giờ, v.v.); C) nó nguyên bản là một phần của RSpec
notaceo

3
Đây là một giải pháp "OK", nhưng chắc chắn be_withinlà giải pháp đúng
Francesco Belladonna

Xin chào Tôi đang sử dụng so sánh ngày giờ với kỳ vọng trễ hẹn công việc expect {FooJob.perform_now}.to have_enqueued_job(FooJob).at(some_time) Tôi không chắc người so .atkhớp sẽ chấp nhận thời gian được chuyển đổi thành số nguyên với .to_ibất kỳ ai đã gặp phải vấn đề này?
Cyril Duchon-Doris 29/09/17

200

Tôi thấy việc sử dụng trình so be_withinkhớp rspec mặc định đẹp hơn:

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

2
.000001 s hơi chật. Tôi đã thử thậm chí .001 và đôi khi nó không thành công. Tôi nghĩ ngay cả 0,1 giây cũng chứng minh rằng thời gian đang được đặt thành bây giờ. Đủ tốt cho mục đích của tôi.
Smoyth

3
Tôi nghĩ câu trả lời này tốt hơn vì nó thể hiện ý định của bài kiểm tra, thay vì che khuất nó đằng sau một số phương pháp không liên quan như to_s. Ngoài ra, to_ito_scó thể không thường xuyên nếu thời gian gần kết thúc một giây.
B Bảy

2
Có vẻ như trình so be_withinkhớp đã được thêm vào RSpec 2.1.0 vào ngày 7 tháng 11 năm 2010 và được nâng cấp một vài lần kể từ đó. rspec.info/documentation/2.14/rspec-expectations/…
Mark Berry

1
Tôi hiểu undefined method 'second' for 1:Fixnum. Có thứ gì tôi cần requirekhông?
David Moles

3
@DavidMoles .secondlà một tiện ích mở rộng đường ray: api.rubyonrails.org/classes/Numeric.html
jwadsack

10

yep như Oinđang gợi ý rằng be_withinmatcher là phương pháp hay nhất

... và nó có một số tiện ích khác -> http://www.eq8.eu/blogs/27-rspec-be_within-matcher

Nhưng có một cách khác để giải quyết vấn đề này là sử dụng Rails được tích hợp sẵn middaymiddnightcác thuộc tính.

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

Bây giờ đây chỉ là để trình diễn!

Tôi sẽ không sử dụng điều này trong bộ điều khiển vì bạn đang khai thác tất cả các cuộc gọi Time.new => tất cả các thuộc tính thời gian sẽ có cùng thời gian => có thể không chứng minh được khái niệm bạn đang cố gắng đạt được. Tôi thường sử dụng nó trong các Đối tượng Ruby có cấu trúc tương tự như sau:

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

Nhưng thành thật mà nói, tôi khuyên bạn nên gắn bó với đối be_withinsánh ngay cả khi so sánh với Time.now.midday!

Vì vậy, có, xin vui lòng gắn bó với be_withinmatcher;)


cập nhật 2017-02

Câu hỏi trong bình luận:

điều gì sẽ xảy ra nếu thời gian ở trong một Hash? bất kỳ cách nào để làm cho kỳ vọng (hash_1) .to eq (hash_2) hoạt động khi một số giá trị hash_1 là trước db-times và các giá trị tương ứng trong hash_2 là post-db-times? -

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

bạn có thể chuyển bất kỳ trình khớp RSpec nào cho trình matchkhớp nối (vì vậy, bạn thậm chí có thể thực hiện kiểm tra API với RSpec thuần túy )

Đối với "post-db-times", tôi đoán ý bạn là chuỗi được tạo sau khi lưu vào DB. Tôi sẽ đề xuất tách trường hợp này thành 2 kỳ vọng (một đảm bảo cấu trúc băm, thứ hai kiểm tra thời gian) Vì vậy, bạn có thể làm điều gì đó như:

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)

Nhưng nếu trường hợp này xảy ra quá thường xuyên trong bộ thử nghiệm của bạn, tôi khuyên bạn nên viết trình đối sánh RSpec của riêng bạn (ví dụ be_near_time_now_db_string) chuyển đổi thời gian chuỗi db thành đối tượng Thời gian và sau đó sử dụng nó như một phần của match(hash):

 expect(hash).to match({mytime: be_near_time_now_db_string})  # you need to write your own matcher for this to work.

điều gì sẽ xảy ra nếu thời gian ở trong một Hash? có cách nào để expect(hash_1).to eq(hash_2)hoạt động khi một số giá trị hash_1 là trước db-times và các giá trị tương ứng trong hash_2 là post-db-times không?
Michael Johnston

Tôi đã nghĩ đến một hàm băm tùy ý (thông số của tôi khẳng định rằng một phép toán không thay đổi bản ghi nào cả). Nhưng cảm ơn!
Michael Johnston

10

Bài cũ, nhưng tôi hy vọng nó sẽ giúp bất cứ ai vào đây cho một giải pháp. Tôi nghĩ sẽ dễ dàng hơn và đáng tin cậy hơn nếu chỉ tạo ngày theo cách thủ công:

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

Điều này đảm bảo ngày được lưu trữ là ngày đúng, mà không cần thực hiện to_xhoặc lo lắng về số thập phân.


Dành thời gian chắc chắn bạn Unfreeze vào cuối spec
Qwertie

Đã thay đổi nó để sử dụng một khối thay thế. Bằng cách đó, bạn không thể quên việc quay trở lại thời gian bình thường.
jBilbo

8

Bạn có thể chuyển đổi đối tượng date / datetime / time thành một chuỗi vì nó được lưu trữ trong cơ sở dữ liệu với 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

Cách dễ nhất mà tôi tìm thấy xung quanh vấn đề này là tạo một current_timephương pháp trợ giúp kiểm tra như sau:

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

Giờ đây, thời gian luôn được làm tròn đến phần nghìn giây gần nhất để so sánh rất đơn giản:

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)rất hữu ích
Koen.

2
Chúng tôi có thể sử dụng .change(usec: 0)thủ thuật này để giải quyết vấn đề mà không cần sử dụng trình trợ giúp đặc tả. Nếu dòng đầu tiên là Timecop.freeze(Time.current.change(usec: 0))thì chúng ta có thể đơn giản so sánh .to eq(Time.now)ở cuối.
Harry Wood,

0

Bởi vì tôi đang so sánh các hàm băm, hầu hết các giải pháp này không phù hợp với tôi vì vậy tôi thấy giải pháp đơn giản nhất là chỉ cần lấy dữ liệu từ hàm băm mà tôi đang so sánh. Vì thời gian cập nhật không thực sự hữu ích cho tôi để kiểm tra điều này hoạt động tốt.

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

expect(data).to eq(
  {updated_at: data[:updated_at], some_other_keys: ...}
)
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.