Kesulitan membandingkan waktu dengan RSpec

119

Saya menggunakan Ruby on Rails 4 dan permata rspec-rails 2.14. Untuk objek saya, saya ingin membandingkan waktu saat ini dengan updated_atatribut objek setelah tindakan pengontrol dijalankan, tetapi saya dalam masalah karena spesifikasi tidak lulus. Artinya, yang diberikan berikut ini adalah kode speknya:

it "updates updated_at attribute" do
  Timecop.freeze

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

Ketika saya menjalankan spesifikasi di atas, saya mendapatkan kesalahan berikut:

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

Bagaimana saya bisa membuat spesifikasi lulus?


Catatan : Saya juga mencoba yang berikut (perhatikan utctambahannya):

it "updates updated_at attribute" do
  Timecop.freeze

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

tetapi spesifikasi masih tidak lulus (perhatikan perbedaan nilai "dapatkan"):

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 ==)
Backo
sumber
Ini membandingkan id objek, maka teks dari inspect cocok, tetapi di bawahnya Anda memiliki dua objek Time yang berbeda. Anda bisa saja menggunakan ===, tapi itu mungkin menderita karena melewati batas kedua. Mungkin yang terbaik adalah menemukan atau menulis matcher Anda sendiri, di mana Anda mengubahnya menjadi epoch detik dan membiarkan perbedaan absolut kecil.
Neil Slater
Jika saya mengerti Anda menghubungkan "melewati batas kedua", masalah seharusnya tidak muncul karena saya menggunakan permata Timecop yang "membekukan" waktu.
Backo
Ah aku melewatkan itu, maaf. Dalam hal ini, gunakan saja ===alih-alih ==- saat ini Anda membandingkan object_id dari dua objek Waktu yang berbeda. Meskipun Timecop tidak akan membekukan waktu server database. . . jadi jika cap waktu Anda dibuat oleh RDBMS, itu tidak akan berfungsi (saya harap itu tidak menjadi masalah bagi Anda di sini)
Neil Slater

Jawaban:

156

Objek Ruby Time mempertahankan presisi yang lebih baik daripada database. Saat nilai dibaca kembali dari database, itu hanya dipertahankan ke presisi mikrodetik, sedangkan representasi dalam memori tepat ke nanodetik.

Jika Anda tidak peduli dengan perbedaan milidetik, Anda dapat melakukan to_s / to_i di kedua sisi ekspektasi Anda

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

atau

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

Lihat ini untuk informasi lebih lanjut tentang mengapa waktunya berbeda

usha
sumber
Seperti yang Anda lihat di kode dari pertanyaan, saya menggunakan Timecoppermata. Haruskah itu menyelesaikan masalah dengan "membekukan" waktu?
Backo
Anda menghemat waktu dalam database dan mengambilnya (@ article.updated_at) yang membuatnya kehilangan nanodetik dimana Time.now mempertahankan nanodetik tersebut. Harus jelas dari beberapa baris pertama jawaban saya
usha
3
Jawaban yang diterima hampir setahun lebih tua dari jawaban di bawah - be_within matcher adalah cara yang jauh lebih baik untuk menangani hal ini: A) Anda tidak memerlukan permata; B) berfungsi untuk semua jenis nilai (integer, float, tanggal, waktu, dll.); C) aslinya merupakan bagian dari RSpec
notaceo
3
Ini adalah solusi yang "OK", tapi yang pasti be_withinadalah yang benar
Francesco Belladonna
Halo Saya menggunakan perbandingan waktu dengan ekspektasi penundaan enqueue pekerjaan expect {FooJob.perform_now}.to have_enqueued_job(FooJob).at(some_time) Saya tidak yakin .atmatcher akan menerima waktu yang dikonversi ke integer dengan .to_isiapa pun yang menghadapi masalah ini?
Cyril Duchon-Doris
200

Saya merasa menggunakan be_withinmatcher rspec default lebih elegan:

expect(@article.updated_at.utc).to be_within(1.second).of Time.now
Oin
sumber
2
0,000001 agak ketat. Saya mencoba bahkan 0,001 dan terkadang gagal. Bahkan 0,1 detik saya pikir membuktikan bahwa waktu sedang diatur ke sekarang. Cukup baik untuk tujuan saya.
smoyth
3
Saya pikir jawaban ini lebih baik karena mengungkapkan maksud dari pengujian, daripada mengaburkannya di balik beberapa metode yang tidak terkait seperti to_s. Juga, to_idan to_smungkin jarang gagal jika waktunya mendekati akhir dari satu detik.
B Tujuh
2
Sepertinya be_withinmatcher telah ditambahkan ke RSpec 2.1.0 pada 7 November 2010, dan ditingkatkan beberapa kali sejak saat itu. rspec.info/documentation/2.14/rspec-expectations/…
Mark Berry
1
Saya mengerti undefined method 'second' for 1:Fixnum. Apakah ada yang perlu saya lakukan require?
David Moles
3
@DavidMoles .secondadalah ekstensi rel: api.rubyonrails.org/classes/Numeric.html
jwadsack
10

ya seperti Oinyang disarankan be_withinmatcher adalah praktik terbaik

... dan memiliki beberapa uscases lagi -> http://www.eq8.eu/blogs/27-rspec-be_within-matcher

Tetapi satu cara lagi untuk mengatasinya adalah dengan menggunakan Rails bawaan middaydan middnightatribut.

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

Sekarang ini hanya untuk demonstrasi!

Saya tidak akan menggunakan ini dalam pengontrol karena Anda menghentikan semua panggilan Time.new => semua atribut waktu akan memiliki waktu yang sama => mungkin tidak membuktikan konsep yang Anda coba capai. Saya biasanya menggunakannya di Objek Ruby yang dibuat seperti ini:

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

Tapi sejujurnya saya merekomendasikan untuk tetap menggunakan be_withinmatcher bahkan saat membandingkan Time.now.midday!

Jadi ya tolong tetap dengan be_withinmatcher;)


perbarui 2017-02

Pertanyaan dalam komentar:

bagaimana jika waktunya ada di Hash? cara apa pun untuk membuat ekspektasi (hash_1) .to eq (hash_2) berfungsi ketika beberapa nilai hash_1 adalah waktu pra-db dan nilai terkait dalam hash_2 adalah waktu-db-pasca? -

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

Anda dapat meneruskan matcher RSpec ke matchmatcher (jadi misalnya Anda bahkan dapat melakukan pengujian API dengan RSpec murni )

Adapun "post-db-times" yang saya maksud adalah string yang dihasilkan setelah disimpan ke DB. Saya akan menyarankan untuk memisahkan kasus ini menjadi 2 ekspektasi (satu memastikan struktur hash, kedua memeriksa waktu) Jadi Anda dapat melakukan sesuatu seperti:

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)

Tetapi jika kasus ini terlalu sering dalam rangkaian pengujian Anda, saya sarankan untuk menulis pencocokan RSpec Anda sendiri (misalnya be_near_time_now_db_string) mengonversi waktu string db ke objek Waktu dan kemudian menggunakan ini sebagai bagian dari match(hash):

 expect(hash).to match({mytime: be_near_time_now_db_string})  # you need to write your own matcher for this to work.
setara8
sumber
bagaimana jika waktunya ada di Hash? cara apa pun untuk membuat expect(hash_1).to eq(hash_2)pekerjaan ketika beberapa nilai hash_1 adalah waktu pra-db dan nilai yang sesuai dalam hash_2 adalah waktu pasca-db?
Michael Johnston
Saya sedang memikirkan hash sewenang-wenang (spesifikasi saya menyatakan bahwa operasi tidak mengubah catatan sama sekali). Tapi terima kasih!
Michael Johnston
10

Posting lama, tapi saya harap ini membantu siapa saja yang masuk ke sini untuk mendapatkan solusi. Saya pikir lebih mudah dan lebih dapat diandalkan untuk hanya membuat tanggal secara manual:

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

Ini memastikan tanggal yang disimpan adalah yang benar, tanpa melakukan to_xatau mengkhawatirkan desimal.

jBilbo.dll
sumber
Pastikan Anda mencairkan waktu di akhir spesifikasi
Qwertie
Menggantinya untuk menggunakan blok sebagai gantinya. Dengan begitu Anda tidak bisa melupakan untuk kembali ke waktu normal.
jBilbo
8

Anda dapat mengonversi objek tanggal / waktu / waktu menjadi string seperti yang disimpan dalam database dengan 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)
Thomas Klemm
sumber
7

Cara termudah yang saya temukan untuk mengatasi masalah ini adalah dengan membuat current_timemetode pembantu tes seperti:

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

Sekarang waktu selalu dibulatkan ke milidetik terdekat untuk perbandingan langsung:

it "updates updated_at attribute" do
  Timecop.freeze(current_time)

  patch :update
  @article.reload
  expect(@article.updated_at).to eq(current_time)
end
Sam
sumber
1
.change(usec: 0)sangat membantu
Koen.
2
Kita bisa menggunakan .change(usec: 0)trik ini untuk menyelesaikan masalah tanpa menggunakan spec helper juga. Jika baris pertama adalah Timecop.freeze(Time.current.change(usec: 0))maka kita dapat membandingkan .to eq(Time.now)di bagian akhir.
Harry Wood
0

Karena saya membandingkan hash, sebagian besar solusi ini tidak berfungsi untuk saya, jadi saya menemukan solusi termudah adalah dengan mengambil data dari hash yang saya bandingkan. Karena waktu updated_at sebenarnya tidak berguna bagi saya untuk menguji ini berfungsi dengan baik.

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

expect(data).to eq(
  {updated_at: data[:updated_at], some_other_keys: ...}
)
Qwertie
sumber