I am using such kind of validation in my rails 3.1 project.
validates_presence_of :sales_price
validates_presence_of :retail_price
validates_numericality_of :sales_price, :greater_than => 0,
:allow_blank => true
validates_numericality_of :retail_price, :greater_than => 0,
:allow_blank => true
validate :sales_price_less_than_retail
def sales_price_less_than_retail
if sales_price >= retail_price
errors.add(:sales_price, "must be less than retail price.")
end
end
I'm testing models using rspec. Everything was ok when i used only rails standard validation helpers. But when i wrote custom validator(sales_price_less_than_retail) tests started to fail.
Here is the code of the test:
it { should validate_presence_of :sales_price }
it { should validate_presence_of :retail_price }
it { should validate_numericality_of :sales_price }
it { should validate_numericality_of :retail_price }
Here is the factory:
Factory.define :offer_option do |f|
f.sales_price rand(21) + 10 # $10-$30
f.retail_price { |a| a.sales_price * 2 }
end
When i run the test i get such errors:
Failures:
1) OfferOption
Failure/Error: it { should validate_presence_of :sales_price }
NoMethodError:
undefined method `>=' for nil:NilClass
# ./app/models/offer_option.rb:38:in `sales_price_less_than_retail'
# ./spec/models/offer_option_spec.rb:18:in `block (2 levels) in <top (required)>'
2) OfferOption
Failure/Error: it { should validate_presence_of :retail_price }
ArgumentError:
comparison of BigDecimal with nil failed
# ./app/models/offer_option.rb:38:in `>='
# ./app/models/offer_option.rb:38:in `sales_price_less_than_retail'
# ./spec/models/offer_option_spec.rb:19:in `block (2 levels) in <top (required)>'
I guess everything should be ok because rspec should test validators separately, but it seems that it calls custom validator after calling validates_presence_of in my test.
The problem disappears when i remove custom validator.
What am I doing wrong?
I assume that this is because validate_presence_of rspec helper set offer_option.sales_price = nil and then call valid? on offer_option. When calling valid?, it runs all your validations, so your custom validation as well. And then you get this error, cause there is no '>=' method on nil.
if you change sales_price_less_than_retail to:
def sales_price_less_than_retail
return if sales_prices.blank? || retail_price.blank?
if sales_price >= retail_price
errors.add(:sales_price, "must be less than retail price.")
end
end
Then it should works.
Related
I have a model (payment) that belongs to another model (event), via a polymorphic association.
Some tests are failing because the owner model (event) is accessed by the payment model in validations, but the event is returning nil. All the features work fine when testing app directly in the browser.
I added some more comments to payment.rb below.
I've tried defining the association in the factories, but no luck.
What is the best way to set up this association in the spec?
# models/event.rb
class Event < ApplicationRecord
has_many :payments, as: :payable, dependent: :destroy
end
# models/payment.rb
class Payment < ApplicationRecord
belongs_to :payable, polymorphic: true
validate :amount_is_valid
def amount_is_valid
if amount.to_i > payable.balance.to_i
errors.add(:amount, "can't be higher than balance")
end
end
end
Both examples in this spec are failing.
# spec/models/payment_spec.rb
require 'rails_helper'
RSpec.describe Payment, type: :model do
let!(:event) { FactoryBot.create(:event, event_type: 'test', total: 10000, balance: 10000) }
let!(:user) {FactoryBot.create(:user)}
let!(:payment) {
FactoryBot.build(:payment,
amount: 300,
method: 'cash',
payer_id: user.id,
payable_id: event.id,
status: 1,
)
}
describe 'Association' do
it do
# This will fail with or without this line
payment.payable = event
is_expected.to belong_to(:payable)
end
end
# Validation
describe 'Validation' do
describe '#amount_is_valid' do
it 'not charge more than event balance' do
# This will make the test pass. The actual spec has a lot more examples though,
# would rather just set the association once.
# payment.payable = event
payment.amount = 5000000
payment.validate
expect(payment.errors[:amount]).to include("can't be higher than balance")
end
end
end
end
Output
# bundle exec rspec spec/models/payment_spec.rb
Randomized with seed 42748
Payment
Association
should belong to payable required: true (FAILED - 1)
Validation
#amount_is_valid
not charge more than event balance (FAILED - 2)
Failures:
1) Payment Association should belong to payable required: true
Failure/Error: if amount.to_i > payable.balance.to_i
NoMethodError:
undefined method `balance' for nil:NilClass
# ./app/models/payment.rb:9:in `amount_is_valid'
# ./spec/models/payment_spec.rb:23:in `block (3 levels) in <top (required)>'
# ./spec/rails_helper.rb:80:in `block (3 levels) in <top (required)>'
# ./spec/rails_helper.rb:79:in `block (2 levels) in <top (required)>'
# ./spec/spec_helper.rb:108:in `block (2 levels) in <top (required)>'
2) Payment Validation #amount_is_valid not charge more than event balance
Failure/Error: if amount.to_i > payable.balance.to_i
NoMethodError:
undefined method `balance' for nil:NilClass
# ./app/models/payment.rb:9:in `amount_is_valid'
# ./spec/models/payment_spec.rb:39:in `block (4 levels) in <top (required)>'
# ./spec/rails_helper.rb:80:in `block (3 levels) in <top (required)>'
# ./spec/rails_helper.rb:79:in `block (2 levels) in <top (required)>'
# ./spec/spec_helper.rb:108:in `block (2 levels) in <top (required)>'
Top 2 slowest examples (0.29972 seconds, 71.6% of total time):
Payment Association should belong to payable required: true
0.28796 seconds ./spec/models/payment_spec.rb:18
Payment Validation #amount_is_valid not charge more than event balance
0.01176 seconds ./spec/models/payment_spec.rb:32
Finished in 0.4186 seconds (files took 4.31 seconds to load)
2 examples, 2 failures
Failed examples:
rspec ./spec/models/payment_spec.rb:18 # Payment Association should belong to payable required: true
rspec ./spec/models/payment_spec.rb:32 # Payment Validation #amount_is_valid not charge more than event balance
Update
Passing specs based on Schwern's feedback.
Still using a custom validation for amount, because balance is a field on the associated payable, not the payment (couldn't find a way to access an associated model from inside a built-in validation helper)
# payment.rb
class Payment < ApplicationRecord
belongs_to :payable, polymorphic: true
validates :payable, presence: true
validate :amount_is_valid
def amount_is_valid
if amount > payable.balance
errors.add(:amount, "can't be greater than balance")
end
end
end
# spec/models/payment_spec.rb
require 'rails_helper'
RSpec.describe Payment, type: :model do
let(:event) { FactoryBot.create(:event, event_type: 'test', total: 10000, balance: 10000) }
let(:user) {FactoryBot.create(:user)}
let(:payment) {
FactoryBot.build(:payment,
amount: 300,
method: 'cash',
payer_id: user.id,
payable: event,
status: 1,
)
}
describe '#payable' do
it 'is an Event' do
expect(payment.payable).to be_a(Event)
end
end
describe '#amount' do
context 'amount is higher than balance' do
before {
payment.amount = payment.payable.balance + 1
}
it 'is invalid' do
payment.validate
expect(payment.errors[:amount]).to include("can't be greater than balance")
end
end
end
end
Your first test is not failing where you think it is. It's failing on the next line, is_expected.to belong_to(:payable).
You're setting payment, but you're testing the implicitly defined subject which will be Payment.new.
is_expected.to belong_to(:payable)
Is equivalent to...
expect(subject).to belong_to(:payable)
And since you have no defined subject this is...
expect(Payment.new).to belong_to(:payable)
Payment.new does not have payable defined and so the amount_is_valid validation errors.
To fix this, test payment directly. And I would suggest staying away from subject while you're learning RSpec. And you should not have to set payment.event, it's already set in the factory.
describe 'Association' do
expect(payment).to belong_to(:payable)
end
But I'm not aware of a belong_to matcher. You should not be directly checking implementation, but rather its behavior. The behavior you want is for payment.payable to return a Payable.
describe '#payable' do
it 'is a Payable' do
expect(payment.payable).to be_a(Payable)
end
end
The second failure is because you have incorrectly initialized your Payment. You're passing in payable_id: event.id but that does not set payable_type. Without payable_type it doesn't know what class the ID is for.
Instead, pass the objects in directly.
let!(:payment) {
FactoryBot.build(:payment,
amount: 300,
method: 'cash',
payer: user,
payable: event,
status: 1,
)
}
Some more general cleanups...
let! will always run the block whether it's used or not. Unless you specifically need that, use let and the blocks will run as needed.
You expect payable to exist, so validate the presence of payable.
Use the built in numericality validator on amount.
class Payment < ApplicationRecord
belongs_to :payable, polymorphic: true
validates :payable, presence: true
validates :amount, numericality: {
less_than_or_equal_to: balance,
message: "must be less than or equal to the balance of #{balance}"
}
end
require 'rails_helper'
RSpec.describe Payment, type: :model do
let(:event) {
create(:event, event_type: 'test', total: 10000, balance: 10000)
}
let(:user) { create(:user) }
let(:payment) {
build(:payment,
amount: 300,
method: 'cash',
payer: user,
payable: event,
status: 1
)
}
# It's useful to organize tests by method.
describe '#payable' do
it 'is a Payable' do
expect(payment.payable).to be_a(Payable)
end
end
describe '#amount' do
# Contexts also help organize and name your tests.
context 'when the amount is higher than the payable balance' do
# This code will run before each example.
before {
# Rather than hard coding numbers, make your tests relative.
# If event.balance changes the test will still work.
payment.amount = payment.payable.balance + 1
}
it 'is invalid' do
expect(payment.valid?).to be false
expect(payment.errors[:amount]).to include("must be less than or equal to")
end
end
end
end
I'm trying to be a good rails developer and write tests as I go. I've run into something I'm unclear on and am looking for advice. I have a model that has a unique case insensitive attribute. The test is failing however. Whats the correct way to test this? What am I doing wrong?
class Tenant < ApplicationRecord
validates :name, presence: true
validates :name, uniqueness: { case_sensitive: false }
end
RSpec.describe Tenant, type: :model do
it { should validate_presence_of :name }
it { should validate_uniqueness_of(:name).case_insensitive }
end
It seems like it's trying to set the id as nil even though we have another validation that requires the presence. But why is it doing that while testing name? I'm confused.
Test shows the following result;
Failures:
1) Tenant Validates Uniqueness of should validate that :name is case-insensitively unique
Failure/Error: self.id = self.id.downcase
NoMethodError:
undefined method `downcase' for nil:NilClass
# ./app/models/tenant.rb:17:in `block in <class:Tenant>'
# /Users/a/.rvm/gems/ruby-2.5.0/gems/shoulda-matchers-3.1.2/lib/shoulda/matchers/active_model/validator.rb:96:in `perform_validation'
# /Users/a/.rvm/gems/ruby-2.5.0/gems/shoulda-matchers-3.1.2/lib/shoulda/matchers/active_model/validator.rb:89:in `validation_result'
# /Users/a/.rvm/gems/ruby-2.5.0/gems/shoulda-matchers-3.1.2/lib/shoulda/matchers/active_model/validator.rb:85:in `validation_error_messages'
# /Users/a/.rvm/gems/ruby-2.5.0/gems/shoulda-matchers-3.1.2/lib/shoulda/matchers/active_model/validator.rb:64:in `messages'
# /Users/a/.rvm/gems/ruby-2.5.0/gems/shoulda-matchers-3.1.2/lib/shoulda/matchers/active_model/validator.rb:25:in `has_messages?'
# /Users/a/.rvm/gems/ruby-2.5.0/gems/shoulda-matchers-3.1.2/lib/shoulda/matchers/active_model/validator.rb:55:in `messages_match?'
# /Users/a/.rvm/gems/ruby-2.5.0/gems/shoulda-matchers-3.1.2/lib/shoulda/matchers/active_model/validator.rb:21:in `call'
# /Users/a/.rvm/gems/ruby-2.5.0/gems/shoulda-matchers-3.1.2/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setters_and_validators.rb:38:in `matches?'
# /Users/a/.rvm/gems/ruby-2.5.0/gems/shoulda-matchers-3.1.2/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setters_and_validators.rb:24:in `each'
# /Users/a/.rvm/gems/ruby-2.5.0/gems/shoulda-matchers-3.1.2/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setters_and_validators.rb:24:in `detect'
# /Users/a/.rvm/gems/ruby-2.5.0/gems/shoulda-matchers-3.1.2/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setters_and_validators.rb:24:in `first_passing'
# /Users/a/.rvm/gems/ruby-2.5.0/gems/shoulda-matchers-3.1.2/lib/shoulda/matchers/active_model/allow_value_matcher.rb:533:in `public_send'
# /Users/a/.rvm/gems/ruby-2.5.0/gems/shoulda-matchers-3.1.2/lib/shoulda/matchers/active_model/allow_value_matcher.rb:533:in `run'
# /Users/a/.rvm/gems/ruby-2.5.0/gems/shoulda-matchers-3.1.2/lib/shoulda/matchers/active_model/allow_value_matcher.rb:400:in `does_not_match?'
# /Users/a/.rvm/gems/ruby-2.5.0/gems/shoulda-matchers-3.1.2/lib/shoulda/matchers/active_model/disallow_value_matcher.rb:32:in `matches?'
# /Users/a/.rvm/gems/ruby-2.5.0/gems/shoulda-matchers-3.1.2/lib/shoulda/matchers/active_model/validation_matcher.rb:155:in `run_allow_or_disallow_matcher'
# /Users/a/.rvm/gems/ruby-2.5.0/gems/shoulda-matchers-3.1.2/lib/shoulda/matchers/active_model/validation_matcher.rb:93:in `disallows_value_of'
# /Users/a/.rvm/gems/ruby-2.5.0/gems/shoulda-matchers-3.1.2/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb:606:in `validate_two_records_with_same_non_blank_value_cannot_coexist?'
# /Users/a/.rvm/gems/ruby-2.5.0/gems/shoulda-matchers-3.1.2/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb:330:in `matches?'
# ./spec/models/tenant_spec.rb:49:in `block (3 levels) in <top (required)>'
Finished in 0.09495 seconds (files took 1.89 seconds to load)
2 examples, 1 failure
Failed examples:
rspec ./spec/models/tenant_spec.rb:49 # Tenant Validates Uniqueness of should validate that :name is case-insensitively unique
Note: this is likely something dumb and obvious that I'm just missing. Your help/advice is appreciated.
Versions:
Ruby 2.50 :: Rails 5.14 :: Rspec 3.7 :: Shoulda-matcher 3.12
As nattfodd mentions:
It seems you have some before/after hooks defined. Please show more code of Tenant model class.
It was a beforehook that was the issue.
I have a class called "Post" that should convert its markdown content to HTML when it is changed or it hasn't been converted yet. I'm trying to use the before_save callback with the if: argument, but I get this error on whatever I pass to the if when I try to run my tests:
Testing started at 1:55 ... rake aborted!
undefined method 'markdown_changed_or_html_nil?' for #<Post:0x000000053603d0>
C:/Users/user/Documents/GitHub/jw/app/models/post.rb:7:in <class:Post>
C:/Users/user/Documents/GitHub/jw/app/models/post.rb:1:in <top (required)>
C:/Users/user/Documents/GitHub/jw/test/test_helper.rb:12:in <class:TestCase>
C:/Users/user/Documents/GitHub/jw/test/test_helper.rb:5:in <top (required)>
C:/Users/user/Documents/GitHub/jw/test/models/post_test.rb:1:in <top (required)>
-e:1:in 'load'
-e:1:in '' Tasks: TOP => test:run => test:units (See full trace by running task with --trace) Run options: --seed 13458
# Running tests:
Finished tests in 0.002000s, 0.0000 tests/s, 0.0000 assertions/s.
0 tests, 0 assertions, 0 failures, 0 errors, 0 skips
Process finished with exit code 1
This is the model in question:
class Post < ActiveRecord::Base
include ActiveModel::Dirty
before_save :convert_markdown, if: :markdown_changed_or_html_nil?
belongs_to :user
validates :user, :title, :content_markdown, { presence: true, on: create }
validates_associated :user
protected
def convert_markdown
markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML, space_after_headers: true, underline: true)
self.content_html = markdown.render(content_markdown)
end
def markdown_changed_or_html_nil?
content_markdown.changed? || content_markdown.nil?
end
end
I'm using Ruby 2.0.0 and Rails 4.0.2.
I might well have made a really really basic mistake - I'm still learning Rails.
Edit: this is post_test.rb
require 'test_helper'
class PostTest < ActiveSupport::TestCase
test 'saving an empty object fails' do
new_post = Post.new
assert_not new_post.save
end
test 'validates that given user id corresponds to user' do
# This will create a user with a given id so we can use the next one up
test_user = User.create({ name: 'Johnny Test', email: 'johnny.test#example.com',
password: 'password', password_confirmation: 'password' })
# Use next id up - there's no reason it should be taken at this point
given_user_id = test_user.id + 1
post_with_invalid_user = Post.new({ title: 'Look at my glorious title', content_markdown: 'Sick content',
user_id: given_user_id })
assert_not post_with_invalid_user.save
end
test 'converts markdown into html' do
# This is a really really basic test just to make sure a conversion happens
new_post = Post.new({ title: 'Check out this markdown, baby', content_markdown: 'I got some *sick* markdown',
user_id: users(:paul).id })
assert_equal '<p>I got some <em>sick</em> markdown</p>', new_post.content_html
end
end
This may not be your primary issue (since your error message doesn't seem to relate to it), but you're not using changed? correctly. changed? needs to be called on your model object, optionally prefixed with your attribute name. So your condition method should look like:
def markdown_changed_or_html_nil?
# based on your method name, shouldn't this be:
# content_markdown_changed? || content_html.nil?
content_markdown_changed? || content_markdown.nil?
end
Find more information about Dirty methods at http://api.rubyonrails.org/classes/ActiveModel/Dirty.html.
ALSO
I'm pretty sure Rails 4 hasn't moved Dirty out of ActiveRecord::Base, so you don't need to manually include ActiveModel::Dirty in your model.
ALSO
This line:
validates :user, :title, :content_markdown, { presence: true, on: create }
Should be:
validates :user, :title, :content_markdown, { presence: true, on: :create }
validates :user, :title, :content_markdown, { presence: true, on: create }
should be
validates :user, :title, :content_markdown, presence: true, on: :create
I think ruby is just interpreting everything after validates :user, :title, :content_markdown as part of the validates function. Why this is, I have to little knowledge of the interpreter to know but ruby has alot of these 'weird' errors. Looking at the linenumbers as CaptChrisD said is always a good start when you encounter them.
I am attempting to write a model test, like so:
require 'spec_helper'
describe Five9List do
before :each do
#five9_list = Five9List.new(name: 'test_list', size: '100')
end
describe "#new" do
it "takes two parameters and returns a Five9List object" do
#five9_list.should be_an_instance_of Five9List
end
end
describe "#name" do
it "returns the correct name" do
#five9_list.name.should eql "test_list"
end
end
describe "#size" do
it "returns the correct size" do
#five9_list.size.should eql 100
end
end
end
Currently, this succeeds and works fine. That's because my model is using attr_accessible, like so:
class Five9List < ActiveRecord::Base
attr_accessible :name, :size
end
If I want to get rid of attr_accessible and follow the rails 4 convention of using strong_params, how would I write that to where my rspec test would still succeed?
Adding this in my controller:
private
def five9_list_params
params.require(:five9_list).permit(:name, :size)
end
And removing attr_accessible does not work.
EDIT
Here is the error I receive from rspec .:
Failures:
1) Five9List#name returns the correct name
Failure/Error: #five9_list.name.should eql "test_list"
expected: "test_list"
got: nil
(compared using eql?)
# ./spec/models/five9_list_spec.rb:16:in `block (3 levels) in <top (required)>'
2) Five9List#size returns the correct size
Failure/Error: #five9_list.size.should eql 100
expected: 100
got: nil
(compared using eql?)
# ./spec/models/five9_list_spec.rb:22:in `block (3 levels) in <top (required)>'
Finished in 0.03303 seconds
4 examples, 2 failures, 1 pending
Failed examples:
rspec ./spec/models/five9_list_spec.rb:15 # Five9List#name returns the correct name
rspec ./spec/models/five9_list_spec.rb:21 # Five9List#size returns the correct size
Randomized with seed 20608
There's nothing wrong with your spec. I can only guess that you're not running Rails 4 or you've installed the ProtectedAttributes gem.
In my model I have the follow to test with:
UNIT_TYPES = [ 'seconds', 'minutes', 'hours', ]
validates_inclusion_of :unit_type, :in => UNIT_TYPES, :allow_blank => true
and using shoulda-matchers I put:
it { should ensure_inclusion_of(:unit_type).in_array(UNIT_TYPES) }
But why do I get this error?
Failures:
1) Price inclusions
Failure/Error: it { should ensure_inclusion_of(:unit_type).in_array(UNIT_TYPES) }
NameError:
uninitialized constant UNIT_TYPES
# ./spec/models/price_spec.rb:39:in `block (3 levels) in <top (required)>'
Whenever you want to call your Model constant out side the Model use <ModelName>::<ConstantVariableName>
Change
UNIT_TYPES
To
User::UNIT_TYPES #Assuming 'User' is your Model Name
So your shoulda code should be something like following
it { should ensure_inclusion_of(:unit_type).in_array(User::UNIT_TYPES) }