Testing that rails model validates date with RSpec - ruby-on-rails

I'm a bit new to Rails and Rspec and as such I'm not sure how to test that date time validations are correct in my model.
I've made a model Event that has start and end times and there's a few imporant conditions on these such as a start time cannot be in the past and the end time must be after the start time.
To ensure these validations I'm using ValidatesTimeliness https://github.com/adzap/validates_timeliness
My model is as follows:
class Event < ActiveRecord::Base
...
validates_datetime :start_date,
:after => :now,
:after_message => "Event cannot start in the past"
validates_datetime :end_date,
:after => :start_date,
:after_message => "End time cannot be before start time"
end
In my RSpec test I have:
describe Event do
let(:event) { FactoryGirl.build :event }
subject { event }
context "when start_date is before the current time" do
it {should_not allow_value(1.day.ago).
for(:start_date)}
end
context "when end_date is before or on start date" do
it {should_not allow_value(event.start_date - 1.day).
for(:end_date)}
it {should_not allow_value(event.start_date).
for(:end_date)}
end
context "when the end_date is after the start_date" do
it {should allow_value(event.start_date + 1.day).
for(:end_date)}
end
end
However this doesn't really test that my start date had to be before the exact date time.
For example if I'd accidentally used :today instead of :now in my model, these tests would also pass.
I read online that there used to be an RSpec matcher called validate_date (http://www.railslodge.com/plugins/1160-validates-timeliness) which would be exactly what I'm looking for but as far as I can tell it's been removed.
My question is how can I improve my tests, do I need to add tests that try the smallest amount of time (i.e. a ms) to ensure the pass/fail accordingly or is there a better way to do it?
Thanks in advance!

You could work with valid? and errors.messages:
Build anEvent that passes validation except for your start_date and end_date
Set the start_date and end_date in the right order, and assert that event.valid? is true
Set the start_date and end_date in the wrong order, and assert that it is not valid? and that event.errors.messages includes the right validation errors. (Note, you have to call event.valid? before checking event.errors.messages, otherwise they will be empty)
Example for valid? and errors.messages:
user = User.new
user.errors.messages #=> {} # no messages, since validations never ran
user.valid? # => false
user.errors.messages #=> {:email=>["can't be blank"]}
user.email = "foo#bar.com"
user.valid? #=> true
user.errors.messages #=> {}

Try this
validates_date :end_time, :after => [:start_time, Proc.new {1.day.from_now_to_date}]
validates_date :start_time, :after => Time.now

Related

Rails update validation only when specific field value is changed

I have written a custom validation function in the rails model. I want to check that validation only a field is changed(in my example the field "activestatus" is changed from "active" to "inactive"). How to do that?
class Normaluser < ApplicationRecord
validate :check_on_update, :on => :update
validate :check_on_status_change :"***what to write here?***"
private
def check_on_update
if is_cyclic?
puts "hierarchy is cyclic"
errors.add(:pid,'it is cyclic')
elsif(get_height(id)+getDepth>=2)
puts "height limit exceeded"
errors.add(:pid,'height limit exceeded')
elsif count_children>=4
errors.add(:pid,'children limit exceeded')
end
puts "all is fine at update"
end
You can use ActiveModel::Dirty
if activestatus_was == 'active' && activestatus == 'inactive'
# validation logic goes here
end
More info: https://api.rubyonrails.org/classes/ActiveModel/Dirty.html
Hope that helps!
Have a look at ActiveModel::Dirty. To only run the validation when activestatus changed from active to inactive just add a line to custom validation method:
def check_on_update
return unless activestatus_changed?(from: 'active', to: 'inactive')
# your validation code
end

Testing custom validation method with RSpec

I try to test validation method that check times overlap for activities.
There are three factories(two of them inherit from activity).
Factories:
activities.rb
FactoryGirl.define do
factory :activity do
name 'Fit Girls'
description { Faker::Lorem.sentence(3, true, 4) }
active true
day_of_week 'Thusday'
start_on '12:00'
end_on '13:00'
pool_zone 'B'
max_people { Faker::Number.number(2) }
association :person, factory: :trainer
factory :first do
name 'Swim Cycle'
description 'Activity with water bicycles.'
active true
day_of_week 'Thusday'
start_on '11:30'
end_on '12:30'
end
factory :second do
name 'Aqua Crossfit'
description 'Water crossfit for evereyone.'
active true
day_of_week 'Thusday'
start_on '12:40'
end_on '13:40'
pool_zone 'C'
max_people '30'
end
end
end
Activities overlaps when are on same day_of_week(activity.day_of_week == first.day_of_week), on same pool_zone(activity.pool_zone == first.pool_zone) and times overlaps.
Validation method:
def not_overlapping_activity
overlapping_activity = Activity.where(day_of_week: day_of_week)
.where(pool_zone: pool_zone)
activities = Activity.where(id: id)
if activities.blank?
overlapping_activity.each do |oa|
if (start_on...end_on).overlaps?(oa.start_on...oa.end_on)
errors.add(:base, "In this time and pool_zone is another activity.")
end
end
else
overlapping_activity.where('id != :id', id: id).each do |oa|
if (start_on...end_on).overlaps?(oa.start_on...oa.end_on)
errors.add(:base, "In this time and pool_zone is another activity.")
end
end
end
end
I wrote rspec test, but unfortunatelly invalid checks.
describe Activity, 'methods' do
subject { Activity }
describe '#not_overlapping_activity' do
let(:activity) { create(:activity) }
let(:first) { create(:first) }
it 'should have a valid factory' do
expect(create(:activity).errors).to be_empty
end
it 'should have a valid factory' do
expect(create(:first).errors).to be_empty
end
context 'when day_of_week, pool_zone are same and times overlap' do
it 'raises an error that times overlap' do
expect(activity.valid?).to be_truthy
expect(first.valid?).to be_falsey
expect(first.errors[:base].size).to eq 1
end
end
end
end
Return:
Failure/Error: expect(first.valid?).to be_falsey
expected: falsey value
got: true
I can't understand why it got true. First create(:activity) should be right, but next shouldn't be executed(overlapping).
I tried add expect(activity.valid?).to be truthy before expect(first.valid?..., but throws another error ActiveRecord::RecordInvalid. Could someone repair my test? I'm newbie with creation tests using RSpec.
UPDATE:
Solution for my problem is not create :first in test but build.
let(:first) { build(:first) }
This line on its own
let(:activity) { create(:activity) }
doesn't create an activity. It only creates an activity, when activity is actually called. Therefore you must call activity somewhere before running your test.
There are several ways to do so, for example a before block:
before { activity }
or you could use let! instead of just let.

Best date validation in Rails 4?

What is the best way to validate a date in Ruby on Rails? I need to make sure the "birthday" is a date, is less than 125 years ago and is not in the future (today is ok).
I have tried three methods:
1) date_validator gem
I used the following code (after installing the gem):
validates :birthday,
date: {
after: Proc.new {Time.now - 125.years}, message: :after,
before: Proc.new {Time.now}, message: :cannot_be_in_the_future
}
This worked except that I could set the date to the number 12 and pass validation.
2) Checking if the date is in a range of dates, in a custom validation method, like so:
from = 125.years.ago.to_date
to = Time.now.to_date
unless (from..to).include? birthday
errors.add(:birthday, :custom_error_msg)
end
This worked well and passed all my tests, but the drawback is that you only get one error message. I would have liked separate error messages for the case when the date is in the future, when it is too long ago and when the input is not a date.
3) Multiple checks in a custom validation method, like so:
begin
birthday.to_date
rescue
errors.add(:birthday, "must be a date")
else
if birthday > Time.now
errors.add(:birtday, "cannot be in the future")
elsif birthday < Time.now - 125.years
errors.add(:birthday, "cannot be over 125 years ago")
end
end
This also passes all my test, and I get different messages as explained above.
So I am wondering, is this the best method? Can it be improved at all (except that the error message text needs work)?
Thanks
For this simple validation, I think following ruby code is enough!
Please check :
validate :is_valid_dob?
private
def is_valid_dob?
if((birthday.is_a?(Date) rescue ArgumentError) == ArgumentError)
errors.add(:birthday, 'Sorry, Invalid Date of Birth Entered.')
end
end
Just use gem 'validates_timeliness'
In your case, using this gem
validates_date :birthday, on_or_after: lambda { 125.years.ago }
You can easily use validates_each method
Just put there 2 validations:
1) For birthday in the past
validates_each :birthday do |record, attr, value|
record.errors.add(attr, 'must be in the past') if value >= Time.now.to_date
end
2) For birthday not more than 150 years ago
validates_each :birthday do |record, attr, value|
record.errors.add(attr, 'must be less than 150 years in the past')
if value <= (Time.now.to_date - 125.years)
end

Comparing dates: "comparison of Date with nil failed"

I have a Client model which has many projects. In the project model I want to validate that the project start date is always before or on the same day as the project end date. This is my project model:
class Project < ActiveRecord::Base
attr_accessible :end_on, :start_on, :title
validates_presence_of :client_id, :end_on, :start_on, :title
validate :start_has_to_be_before_end
belongs_to :clients
def start_has_to_be_before_end
if start_on > end_on
errors[:start_on] << " must not be after end date."
errors[:end_on] << " must not be before start date."
end
end
end
My application works as expected and gives me the specified errors in case the validation fails.
However, in my unit test for the projects, I am trying to cover this scenario, deliberately setting the start date after the end date:
test "project must have a start date thats either on the same day or before the end date" do
project = Project.new(client_id: 1, start_on: "2012-01-02", end_on: "2012-01-01", title: "Project title")
assert !project.save, "Project could be saved although its start date was after its end date"
assert !project.errors[:start_on].empty?
assert !project.errors[:end_on].empty?
end
Strangely, running this test gives me three errors, all referring to this line if start_on > end_on in my validation method, saying undefined method '>' for nil:NilClass twice and comparison of Date with nil failed once.
What can I do to make the tests pass?
You are creating a Project that has string values for :start_on and :end_on. That's unlikely to work. Rails might try to be smart and parse those, I'm not sure.. I wouldn't count on it. Odds are some coercion is going on and the values are getting set to nil.
I would do this:
project = Project.new(client_id: 1,
start_on: 2.days.from_now.to_date,
end_on: Time.now.to_date,
title: "Project title")

Unit Testing validates presence of odd behaviour

I'm trying out the whole TDD and I'm running into a problems with validate presence. I have a model called Event and I want to ensure that when an Event is created that a title a price and a summary exists.
Unit Test Code
class EventTest < ActiveSupport::TestCase
test "should not save without a Title" do
event = Event.new
event.title = nil
assert !event.save, "Save the Event without title"
end
test "should not save without a Price" do
event = Event.new
event.price = nil
assert !event.save, "Saved the Event without a Price"
end
test "should not save without a Summary" do
event = Event.new
event.summary = nil
assert !event.save, "Saved the Event without a Summary"
end
end
I run the test I get 3 FAILS. Which is Good.
Now I want to to just get the title test to pass first with the following code in the Event model.
class Event < ActiveRecord::Base
validates :title, :presence => true
end
When I re-run the test I get 3 PASSES where I would think I should have gotten 1 PASS and 2 FAILS. Why am I getting 3 PASSES?
I have two test helper methods that can make this sort of thing easier to diagnose:
def assert_created(model)
assert model, "Model was not defined"
assert_equal [ ], model.errors.full_messages
assert model.valid?, "Model failed to validate"
assert !model.new_record?, "Model is still a new record"
end
def assert_errors_on(model, *attrs)
found_attrs = [ ]
model.errors.each do |attr, error|
found_attrs << attr
end
assert_equal attrs.flatten.collect(&:to_s).sort, found_attrs.uniq.collect(&:to_s).sort
end
You'd use them in cases like this:
test "should save with a Title, Price or Summary" do
event = Event.create(
:title => 'Sample Title',
:price => 100,
:summary => 'Sample summary...'
)
assert_created event
end
test "should not save without a Title, Price or Summary" do
event = Event.create
assert_errors_on event, :title, :price, :summary
end
This should show if you're missing a validation that you expected and will also give you feedback on specific validations that have failed when not expected.
When you created the model with Event.new, all attributes initially have a value of nil. This means that all 3 attributes you are checking are already nil (so event.title = nil and event.price = nil don't actually do anything). Since title has been marked for validation to ensure its presence, unless you set title to something other than nil, you will not be able to save the model.
Perhaps try adding this to your test class:
setup do
#event_attributes = {:title => "A title", :price => 3.99, :summary => "A summary"}
end
Then instead of:
event = Event.new
event.title = nil
Use:
event = Event.new(#event_attributes.merge(:title => nil))
Do the same for all your tests (substituting :title with whatever attribute you are validating presence for)
Also, there's no reason to call save to test for a valid state. You can just call event.valid? to avoid trips to the database where not needed.

Resources