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.
Related
I'm trying to use rails Faker gem to produce unique product names to make sample Item models in the database. I've used Faker multiple times but for some reason I can't produce new product names. I've made the nameMaker function to avoid possible early repeats, but I get a record invalidation just after one insert. Does anyone know how I could fix this?
seed.rb:
98.times do |n|
name = Item.nameMaker
description = Faker::Lorem.sentence(1)
price = Item.priceMaker
item = Item.create!(
name: name,
description: description,
price: price)
end
item.rb:
class Item < ActiveRecord::Base
validates :name, presence: true, length: { maximum: 100 }
validates :description, presence: true,
length: { maximum: 1000 }
VALID_PRICE_REGEX = /\A\d+(?:\.\d{0,3})?\z/
validates :price, presence: true,
:format => { with: VALID_PRICE_REGEX },
:numericality => {:greater_than => 0}
validates_uniqueness_of :name
def Item.nameMaker
loop do
name = Item.newName
break if Item.find_by(name: name).nil?
end
return name
end
def Item.newName
Faker::Commerce.product_name
end
end
To get a unique name, enclose the faker in brackets. Eg
name { Faker::Commerce.product_name }
To achieve this, you could also make use of factory girl and when you want to create 98 different Items, you could have something like
factories/item.rb
FactoryGirl.define do
factory :item do
name { Faker::Commerce.product_name }
description { Faker::Lorem.sentence(1) }
price Faker::Commerce.price
end
end
in your spec file
let(:item) { create_list(:item, 98) }
You can add validates_uniqueness_of :name in your model. When you run seed method if there is already exists same name, it will throw error and skip to the next.
There is possibility that you will not have exactly 98 Items. You can increase number of times or edit Faker itself.
I figured it out after some experimentation, apparently the loop in some ways acts as like a function in terms of scoping. If you initialize a local variable in a loop, the function outside of the loop will not see it. In this case name always returning the string Item from the Item.nameMaker function. Thus the first attempt would always succeed and the second one would obtain the validation restriction.
def Item.nameMaker
loop do
name = Faker::Commerce.product_name # 'Random Product Name'
puts "Name: #{name}" # "Name: Random Product Name"
item = Item.find_by(name: name)
if item.nil?
puts "#{name} not found" # "Random Product Name not found"
break
else
end
end
puts "Returning Name #{name}" # "Returning Name Item"
return name
end
I managed to fix this by initializing the local variable outside of the loop. By doing this the entire function now has visibility to the same local variable for some reason.
def Item.nameMaker
name = "" #initializing
loop do
name = Faker::Commerce.product_name # 'Random Product Name'
puts "Name: #{name}" # "Name: Random Product Name"
item = Item.find_by(name: name)
if item.nil?
puts "#{name} not found" # "Random Product Name not found"
break
else
end
end
puts "Returning Name #{name}" # "Returning Random Product Name"
return name
end
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
I have the following two tests for a Rails app I'm working on.
This one fails:
test "should save comment without parent comment" do
comment = Comment.new(:text => "hello world")
comment.user = users(:charlie)
comment.story = stories(:one)
assert comment.save
end
And this one passes:
test "should save comment without parent comment" do
comment = Comment.new(:text => "hello world")
comment.user = users(:charlie)
comment.story = stories(:one)
comment.save
assert comment.save
end
When I change the assert line in the first (failing) test to this:
assert comment.save, Proc.new { "#{comment.errors.messages.inspect} -- valid? #{comment.valid?} -- save? #{comment.save}" }
It prints out this as the error message:
{} -- valid? true -- save? true
I'm completely at a loss as to why my Comment model requires two calls to save. It works fine in my controller with just one call to save.
I have an after_create callback, but I don't think it should be affecting anything (as I said, this all works fine from my controller):
after_create :create_upvote_from_user
def create_upvote_from_user
Vote.create! :user => user, :vote => 1, "#{self.class.name.underscore}_id" => id
end
In my model example Game, has a status column. But I usually set status by using symbols. Example self.status = :active
MATCH_STATUS = {
:betting_on => "Betting is on",
:home_team_won => "Home team has won",
:visiting_team_won => "Visiting team has one",
:game_tie => "Game is tied"
}.freeze
def viewable_status
MATCH_STATUS[self.status]
end
I use the above Map to switch between viewable status and viceversa.
However when the data gets saved to db, ActiveRecord appends "--- " to each status. So when I retrieve back the status is screwed.
What should be the correct approach?
Override the getter and the setter:
def status
read_attribute(:status).to_sym
end
def status=(new_status)
write_attribute :status, new_status.to_s
end
I'm looking for clean and short code to test validations in Rails Unittests.
Currently I do something like this
test "create thing without name" do
assert_raise ActiveRecord::RecordInvalid do
Thing.create! :param1 => "Something", :param2 => 123
end
end
I guess there is a better way that also shows the validation message?
Solution:
My current solution without an additional frameworks is:
test "create thing without name" do
thing = Thing.new :param1 => "Something", :param2 => 123
assert thing.invalid?
assert thing.errors.on(:name).any?
end
You don't mention what testing framework that you're using. Many have macros that make testing activerecord a snap.
Here's the "long way" to do it without using any test helpers:
thing = Thing.new :param1 => "Something", :param2 => 123
assert !thing.valid?
assert_match /blank/, thing.errors.on(:name)
In newer versions of Rails (v5) with MiniTest
test "create thing without name" do
thing = Thing.new :param1 => "Something", :param2 => 123
assert thing.invalid?
assert thing.errors.added? :name, :blank
end
https://devdocs.io/rails~5.2/activemodel/errors
I'm using Rails 2.0.5, and when I want to assert that a model will fail validation, I check the errors.full_messages method, and compare it to an array of expected messages.
created = MyModel.new
created.field1 = "Some value"
created.field2 = 123.45
created.save
assert_equal(["Name can't be blank"], created.errors.full_messages)
To assert that validation succeeds, I just compare to an empty array. You can do something very similar to check that a Rails controller has no error messages after a create or update request.
assert_difference('MyModel.count') do
post :create, :my_model => {
:name => 'Some name'
}
end
assert_equal([], assigns(:my_model).errors.full_messages)
assert_redirected_to my_model_path(assigns(:my_model))
For those using Rails 3.2.1 and up, I prefer using the added? method:
assert record.errors.added? :name, :blank
I use a test helper that looks like this:
def assert_invalid(record, options)
assert_predicate record, :invalid?
options.each do |attribute, message|
assert record.errors.added?(attribute, message), "Expected #{attribute} to have the following error: #{message}"
end
end
Which allows me to write tests like this:
test "should be invalid without a name" do
user = User.new(name: '')
assert_invalid user, name: :blank
end
Try also accept_values_for gem.
It allows to do something like this:
describe User do
subject { User.new(#valid_attributes)}
it { should accept_values_for(:email, "john#example.com", "lambda#gusiev.com") }
it { should_not accept_values_for(:email, "invalid", nil, "a#b", "john#.com") }
end
In this way you can test really complicated validations easily
You could give the rspec-on-rails-matchers a try. Provides you with syntax like:
#thing.should validates_presence_of(:name)
Not sure when it was added but the #where method makes it easy to target a specific error without having to rely on the text of the message.
refute #thing.valid?
assert #thing.errors.where(:name, :invalid).present?