RSpec failing - date comparison

Viewed 370

So I have an RSpec test that fails, I'm sure that the test is the issue as from a functionality perspective it works fine.

In short, end_time should not be before_start time. Upon saving the model this works correctly, only the RSpec is failing, any pointers would be greatly appreciated

chain_plan_spec.rb

# frozen_string_literal: true

# == Schema Information
#
# Table name: chain_plans
#
#  id         :bigint           not null, primary key
#  end_time   :datetime
#  start_time :datetime
#  created_at :datetime         not null
#  updated_at :datetime         not null
#  faction_id :bigint           not null
#
# Indexes
#
#  index_chain_plans_on_faction_id  (faction_id)
#
# Foreign Keys
#
#  fk_rails_...  (faction_id => factions.id)
#
require 'rails_helper'
RSpec.describe ChainPlan, type: :model do
  it { is_expected.to validate_presence_of :start_time }
  it { is_expected.to validate_presence_of :end_time }
  it { is_expected.to validate_presence_of :faction }

  it 'does not have a start date/time before an end date/time' do
    cp = described_class.new
    cp.start_time = Date.now
    cp.end_time = cp.start - 1.minute
    cp.save!

    expect(cp).not_to be_valid
  end
  # TODO: end is 0 minute
  # TODO start is 0 minute
end

chain_plan.rb

# frozen_string_literal: true

# == Schema Information
#
# Table name: chain_plans
#
#  id         :bigint           not null, primary key
#  end_time   :datetime
#  start_time :datetime
#  created_at :datetime         not null
#  updated_at :datetime         not null
#  faction_id :bigint           not null
#
# Indexes
#
#  index_chain_plans_on_faction_id  (faction_id)
#
# Foreign Keys
#
#  fk_rails_...  (faction_id => factions.id)
#
class ChainPlan < ApplicationRecord
  belongs_to :faction
  has_many :chain_plan_slots
  validates :start_time, presence: { on: :create, message: "can't be blank" }
  validates :end_time, presence: { on: :create, message: "can't be blank" }
  validates :faction, presence: { on: :create, message: "can't be blank" }
  validate :end_date_after_start_date?

  private

  def end_date_after_start_date?
    return if end_time.blank? || start_time.blank?

    errors.add(:end_date, 'must be after the start date') if end_time < start_time
  end
end
2 Answers

When testing validations don't use expect(object).not_to be_valid or expect(object).to be_valid. This is just a recipe for both false positivies and negatives as you're not actually testing a single behavior - rather you're testing every single validation at once along with your test setup.

RSpec.describe ChainPlan, type: :model do
  it 'does not allow an end_time which is after the start_time' do
    cp = described_class.new(
      start_time: Time.current,
      end_time: Time.current - 1.minute
    )
    cp.valid?
    expect(cp.errors.messages_for(:end_time)).to include 'must be after the start date'
  end
  it "allows a valid start_time/end_time combo" do
    cp = described_class.new(
      start_time: Time.current,
      end_time: Time.current.advance(10.minutes)
    )
    cp.valid?
    expect(cp.errors).to_not have_key :end_date
  end
end

Instead setup the object and then call valid? on it to trigger the validations. Write expectations on the errors object to test the actual behavior instead of carpet bombing. For example here you would have completely missed that you where adding the errors to the key :end_date instead of :end_time.

The validation itself also can be improved:

class ChainPlan < ApplicationRecord
  belongs_to :faction
  has_many :chain_plan_slots
  validates :start_time, presence: { on: :create, message: "can't be blank" }
  validates :end_time, presence: { on: :create, message: "can't be blank" }
  validates :faction, presence: { on: :create, message: "can't be blank" }
  validate :end_date_after_start_date?, if: ->{ end_time.present? && start_time.present? }

  private

  def end_date_after_start_date?
    errors.add(:end_time, 'must be after the start date') if end_time < start_time
  end
end

I think the issue is Date.now, which is not a proper command - at least from documentation I could not find the now method for the class Date. Use Time.now instead. This worked fine for me.

Related