มีปัญหาในการเปรียบเทียบเวลากับ RSpec


119

ฉันใช้ Ruby บน Rails 4 และ rspec-rail 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 ==)

ฉันจะทำ spec ให้ผ่านได้อย่างไร?


หมายเหตุ : ฉันลองทำสิ่งต่อไปนี้แล้วด้วย (สังเกตการutcเพิ่มเติม):

it "updates updated_at attribute" do
  Timecop.freeze

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

แต่ข้อมูลจำเพาะยังไม่ผ่าน (สังเกตความแตกต่างของค่า "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 ==)

เป็นการเปรียบเทียบรหัสวัตถุดังนั้นข้อความจากการตรวจสอบจึงตรงกัน แต่ข้างใต้คุณมีวัตถุเวลาที่แตกต่างกันสองรายการ คุณสามารถใช้ได้===แต่อาจต้องทนทุกข์ทรมานจากการข้ามขอบเขตที่สอง วิธีที่ดีที่สุดคือการค้นหาหรือเขียนตัวจับคู่ของคุณเองซึ่งคุณจะแปลงเป็นช่วงวินาทีและอนุญาตให้มีความแตกต่างที่แน่นอนเล็กน้อย
Neil Slater

ถ้าฉันเข้าใจคุณเกี่ยวกับ "การข้ามขอบเขตที่สอง" ปัญหาไม่ควรเกิดขึ้นเนื่องจากฉันใช้อัญมณีTimecopที่ "หยุดเวลา"
Backo

อ๊ะฉันพลาดไปขอโทษ ในกรณีนี้ให้ใช้===แทน==- ขณะนี้คุณกำลังเปรียบเทียบ object_id ของวัตถุ Time สองชิ้นที่ต่างกัน แม้ว่า Timecop จะไม่ตรึงเวลาเซิร์ฟเวอร์ฐานข้อมูล . . ดังนั้นหากการประทับเวลาของคุณถูกสร้างขึ้นโดย RDBMS มันจะไม่ทำงาน (ฉันคิดว่านั่นไม่ใช่ปัญหาสำหรับคุณที่นี่)
Neil Slater

คำตอบ:


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)

อ้างถึงนี้สำหรับข้อมูลเพิ่มเติมเกี่ยวกับสาเหตุที่เวลาจะแตกต่างกัน


ดังที่คุณเห็นในรหัสจากคำถามฉันใช้Timecopอัญมณี ควรแก้ปัญหาด้วยการ "หยุด" เวลาหรือไม่
Backo

คุณประหยัดเวลาในฐานข้อมูลและดึงข้อมูล (@ article.updated_at) ซึ่งทำให้เวลาในหน่วยนาโนวินาทีหลุดออกไปโดยที่ Time.now ยังคงรักษาระดับนาโนวินาทีไว้ ควรจะชัดเจนจากสองสามบรรทัดแรกของคำตอบของฉัน
usha

3
คำตอบที่ได้รับการยอมรับนั้นเก่ากว่าคำตอบด้านล่างเกือบหนึ่งปี - ตัวจับคู่ be_within เป็นวิธีที่ดีกว่ามากในการจัดการสิ่งนี้: A) คุณไม่จำเป็นต้องมีอัญมณี B) ใช้งานได้กับค่าประเภทใดก็ได้ (จำนวนเต็มทศนิยมวันที่เวลา ฯลฯ ); C) เป็นส่วนหนึ่งของ RSpec
notaceo

3
นี่เป็นวิธีแก้ปัญหาที่ "ใช้ได้" แต่be_withinเป็นวิธีที่ถูกต้องแน่นอน
Francesco Belladonna

สวัสดีฉันใช้การเปรียบเทียบวันที่และเวลากับความคาดหวังความล่าช้าในการจัดคิวงาน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 เซเว่น

2
ดูเหมือนว่าตัวbe_withinจับคู่จะถูกเพิ่มใน RSpec 2.1.0 เมื่อวันที่ 7 พฤศจิกายน 2010 และได้รับการปรับปรุงอีกสองสามครั้งตั้งแต่นั้นมา rspec.info/documentation/2.14/rspec-expectations/…
Mark Berry

1
ฉันเข้าใจundefined method 'second' for 1:Fixnumแล้ว มีบางอย่างที่ฉันต้องการrequireหรือไม่?
David Moles

3
@DavidMoles .secondเป็นส่วนขยายราง: api.rubyonrails.org/classes/Numeric.html
jwadsack

11

โพสต์เก่า แต่ฉันหวังว่ามันจะช่วยทุกคนที่เข้ามาที่นี่เพื่อหาแนวทางแก้ไข ฉันคิดว่าการสร้างวันที่ด้วยตนเองนั้นง่ายและน่าเชื่อถือกว่า:

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

10

ใช่ตามที่Oinแนะนำให้be_withinจับคู่เป็นแนวทางปฏิบัติที่ดีที่สุด

... และมี 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

ตอนนี้เป็นเพียงการสาธิตเท่านั้น!

ฉันจะไม่ใช้สิ่งนี้ในคอนโทรลเลอร์เนื่องจากคุณกำลังหยุดทำงานตลอดเวลาการโทรใหม่ => แอตทริบิวต์เวลาทั้งหมดจะมีเวลาเท่ากัน => อาจไม่สามารถพิสูจน์แนวคิดที่คุณพยายามบรรลุได้ ฉันมักจะใช้ใน Ruby Objects ที่ประกอบด้วยสิ่งนี้:

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_withinตัวจับคู่แม้ว่าจะเปรียบเทียบ Time.now.midday!

ใช่แล้วกรุณาติดกับbe_withinmatcher;)


อัปเดต 2017-02

คำถามในความคิดเห็น:

จะเกิดอะไรขึ้นถ้าเวลาอยู่ใน Hash? วิธีใดที่จะทำให้คาดหวัง (hash_1) .to eq (hash_2) ทำงานได้เมื่อค่า hash_1 บางค่าเป็นค่าก่อน db-times และค่าที่เกี่ยวข้องใน hash_2 เป็น post-db-times? -

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

คุณสามารถส่งตัวจับคู่ RSpec ไปยังตัวmatchจับคู่ได้ (เช่นคุณสามารถทำการทดสอบ API ด้วย RSpec บริสุทธิ์ได้ )

สำหรับ "post-db-times" ฉันเดาว่าคุณหมายถึงสตริงที่สร้างขึ้นหลังจากบันทึกลงใน DB ฉันขอแนะนำให้แยกกรณีนี้ออกเป็น 2 ความคาดหวัง (หนึ่งในการตรวจสอบโครงสร้างแฮชครั้งที่สองตรวจสอบเวลา) ดังนั้นคุณสามารถทำสิ่งต่างๆเช่น:

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 matcherของคุณเอง (เช่นbe_near_time_now_db_string) การแปลงเวลาสตริง db เป็น Time object จากนั้นใช้สิ่งนี้เป็นส่วนหนึ่งของmatch(hash):

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

จะเกิดอะไรขึ้นถ้าเวลาอยู่ใน Hash? วิธีใดที่จะทำให้expect(hash_1).to eq(hash_2)ทำงานได้เมื่อค่า hash_1 บางค่าเป็นค่าก่อน db-times และค่าที่เกี่ยวข้องใน hash_2 เป็น post-db-times?
Michael Johnston

ฉันกำลังคิดแฮชโดยพลการ (ข้อมูลจำเพาะของฉันยืนยันว่าการดำเนินการไม่ได้เปลี่ยนบันทึกเลย) แต่ขอบคุณ!
Michael Johnston

8

คุณสามารถแปลง datetime / / 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.