RSpec ile zamanı karşılaştırırken sorun


119

Ruby on Rails 4 ve rspec-rails gem 2.14 kullanıyorum. Bir nesnem için, updated_atbir denetleyici eylemi çalıştırıldıktan sonra geçerli saati nesne özniteliğiyle karşılaştırmak istiyorum , ancak teknik özellik geçmediği için başım belada. Yani, aşağıdaki verilen özellik kodudur:

it "updates updated_at attribute" do
  Timecop.freeze

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

Yukarıdaki özellikleri çalıştırdığımda aşağıdaki hatayı alıyorum:

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

Şartnamenin geçmesini nasıl sağlayabilirim?


Not : Aşağıdakileri de denedim (eklemeye dikkat edin utc):

it "updates updated_at attribute" do
  Timecop.freeze

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

ancak teknik özellik hala geçmiyor ("var" değer farkına dikkat edin):

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

Nesne kimliklerini karşılaştırıyor, dolayısıyla incelemeden gelen metin eşleşiyor, ancak altında iki farklı Zaman nesneniz var. Sadece kullanabilirsiniz ===, ancak bu, ikinci sınırları aşmaktan zarar görebilir. Muhtemelen en iyisi, epoch saniyelerine dönüştüğünüz ve küçük bir mutlak farka izin verdiğiniz kendi eşleştiricinizi bulmak veya yazmaktır.
Neil Slater

"İkinci sınırları aşma " ile ilgili olduğunuzu anladıysam , zamanı "donduran" Timecop mücevherini kullandığım için sorun ortaya çıkmamalıdır .
Backo

Ah bunu kaçırdım, üzgünüm. Bu durumda, ===bunun yerine ==- şu anda iki farklı Time nesnesinin object_id'sini karşılaştırıyorsunuz. Timecop veritabanı sunucusu zamanını dondurmasa da. . . bu nedenle, zaman damgalarınız RDBMS tarafından oluşturuluyorsa, işe yaramaz (bunun sizin için burada bir sorun olmadığını
umuyorum

Yanıtlar:


156

Ruby Time nesnesi, veritabanından daha fazla kesinlik sağlar. Değer, veritabanından geri okunduğunda, yalnızca mikrosaniye hassasiyetinde korunurken, bellek içi gösterim nanosaniye hassasiyetinde olur.

Milisaniye farkını umursamıyorsanız, beklentinizin her iki tarafında da bir to_s / to_i yapabilirsiniz.

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

veya

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

Bakın bu kez farklı neden hakkında daha fazla bilgi için


Sorudaki kodda da görebileceğiniz gibi, Timecopmücevheri kullanıyorum. Sadece zamanı "dondurarak" sorunu çözmeli mi?
Backo

Veritabanında zamandan tasarruf edersiniz ve onu (@ article.updated_at) alırsınız, bu da onu nanosaniyeleri kaybettirir, Time.now'un nanosaniyeyi koruduğu yerde. Cevabımın ilk birkaç satırından açıkça anlaşılmalı
usha

3
Kabul edilen cevap aşağıdaki cevaptan neredeyse bir yıl daha eski - be_within eşleştirici bunu halletmenin çok daha iyi bir yoludur: A) bir cevhere ihtiyacınız yok; B) herhangi bir değer türü için çalışır (tam sayı, kayan nokta, tarih, saat vb.); C) yerel olarak
RSpec'in bir

3
Bu "Tamam" bir çözüm, ancak kesinlikle be_withindoğru çözüm
Francesco Belladonna

Merhaba, iş izleme gecikmesi beklentileriyle tarih-saat karşılaştırmaları kullanıyorum expect {FooJob.perform_now}.to have_enqueued_job(FooJob).at(some_time) , .ateşleştiricinin tam sayıya dönüştürülen bir zamanı kabul edeceğinden emin değilim , .to_iherkes bu sorunla karşılaştı mı?
Cyril DUCHON-Doris

200

be_withinVarsayılan rspec eşleştiriciyi kullanmayı daha zarif buluyorum :

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

2
.000001 s biraz sıkı. .001'i bile denedim ve bazen başarısız oluyordu. Sanırım 0,1 saniye bile zamanın şimdiye ayarlandığını kanıtlıyor. Benim amaçlarım için yeterince iyi.
smoyth

3
Bu cevabın daha iyi olduğunu düşünüyorum çünkü testin amacını ifade ediyor, gibi bazı ilgisiz yöntemlerin arkasına gizlemek yerine to_s. Ayrıca, to_ive to_ssüresi bir saniyenin sonuna yakın ise seyrek başarısız olabilir.
B Yedi

2
be_withinEşleştirici, 7 Kasım 2010'da RSpec 2.1.0'a eklenmiş ve o zamandan beri birkaç kez geliştirilmiş gibi görünüyor . rspec.info/documentation/2.14/rspec-expectations/…
Mark Berry

1
Anlıyorum undefined method 'second' for 1:Fixnum. Yapmam gereken bir şey var mı require?
David Moles

3
@DavidMoles .secondbir ray uzantısıdır: api.rubyonrails.org/classes/Numeric.html
jwadsack

11

Eski gönderi, ama umarım bir çözüm için buraya giren herkese yardımcı olur. Tarihi manuel olarak oluşturmanın daha kolay ve daha güvenilir olduğunu düşünüyorum:

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

Bu, to_xondalık sayılar konusunda endişelenmeden veya yapmadan depolanan tarihin doğru tarih olmasını sağlar .


Spesifikasyonun sonunda zamanı
çözdüğünüzden

Bunun yerine bir blok kullanacak şekilde değiştirildi. Bu şekilde normal zamana dönmeyi unutamazsınız.
jBilbo

10

evet, eşleştiricinin en iyi uygulama Oinolduğunu öne sürüyorbe_within

... ve daha fazla uscases var -> http://www.eq8.eu/blogs/27-rspec-be_within-matcher

Ancak bununla nasıl başa çıkılacağının bir başka yolu, yerleşik Rails middayve middnightnitelikleri kullanmaktır.

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

Şimdi bu sadece gösteri için!

Tüm Time.new çağrıları => tüm zaman özellikleri aynı zamana sahip olacak => ulaşmaya çalıştığınız kavramı kanıtlayamayabilir. Bunu genellikle buna benzer oluşturulmuş Ruby Nesnelerinde kullanırım:

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

Ama dürüst olmak gerekirse, be_withinTime.now.midday'ı karşılaştırırken bile eşleştiriciye bağlı kalmanızı tavsiye ederim !

Yani evet pls be_withineşleştirici ile sopa ;)


güncelleme 2017-02

Yorumdaki soru:

ya zamanlar bir Hash'teyse? (hash_1) .to eq (hash_2) 'nin bazı hash_1 değerleri db-zamanlar öncesi ve hash_2'deki ilgili değerler post-db-zamanlar olduğunda çalışmasını sağlamanın herhangi bir yolu var mı? -

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

herhangi bir RSpec eşleştiriciyi eşleştiriciye aktarabilirsiniz match(böylece örneğin saf RSpec ile API testi bile yapabilirsiniz )

"Post-db-times" ile ilgili olarak, sanırım DB'ye kaydettikten sonra oluşturulan dizeyi kastediyorsunuz. Bu vakayı 2 beklentiye ayırmanızı öneririm (biri hash yapısını garanti eder, ikincisi zamanı kontrol eder) Böylece şöyle bir şey yapabilirsiniz:

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)

Ancak bu durum test paketinizde çok sık görülüyorsa , kendi RSpec eşleştiricinizi yazmanızı (örneğin be_near_time_now_db_string) db dizgi zamanını Time nesnesine dönüştürmenizi ve ardından bunu aşağıdakilerin bir parçası olarak kullanmanızı öneririm match(hash):

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

ya zamanlar bir Hash'teyse? expect(hash_1).to eq(hash_2)bazı hash_1 değerleri db-zamanlar öncesi ve hash_2'deki ilgili değerler post-db-zamanlar olduğunda çalışma yapmanın bir yolu var mı?
Michael Johnston

Keyfi bir hash düşünüyordum (benim şartnamem bir işlemin bir kaydı hiç değiştirmediğini iddia ediyor). Ama teşekkürler!
Michael Johnston

8

Tarih / tarih / saat nesnesini, veritabanında depolandığı haliyle bir dizeye dönüştürebilirsiniz 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

Bu sorunun etrafında bulduğum en kolay yol, aşağıdaki current_timegibi bir test yardımcı yöntemi oluşturmaktır :

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

Şimdi, zaman her zaman en yakın milisaniyeye yuvarlanır, çünkü karşılaştırmalar basittir:

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)çok yardımcı oluyor
Koen.

2
Bu .change(usec: 0)numarayı, bir teknik yardımcısı da kullanmadan sorunu çözmek için kullanabiliriz . İlk satır ise , sonunda Timecop.freeze(Time.current.change(usec: 0))basitçe karşılaştırabiliriz .to eq(Time.now).
Harry Wood

0

Karmaları karşılaştırdığım için, bu çözümlerin çoğu benim için işe yaramadı, bu yüzden en kolay çözümün, karşılaştırdığım karmadan verileri almak olduğunu buldum. Güncellenen_at süreleri test etmem için gerçekten yararlı olmadığından, bu iyi çalışıyor.

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

expect(data).to eq(
  {updated_at: data[:updated_at], some_other_keys: ...}
)
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.