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) }
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 learning Ruby on rails using http://railstutorial.org/ . While going through chapter 6 in his book. I came across my user fields becoming nil. Which in turn causes the authenticate method to fail.
FYI I have used rails 5 and ruby 2.6 all lastest Gems unlike the one mentioned in the tutorial.
This is Rspec results after running the tests
$bundle exec rspec spec/
including Capybara::DSL in the global scope is not recommended!
Randomized with seed 55216
............F..........FFF..F...
Failures:
1) User should be valid
Failure/Error: it { should be_valid }
expected #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil, password_digest: nil> to be valid, but got errors: Password can't be blank
# ./spec/models/user_spec.rb:27:in `block (2 levels) in <top (required)>'
2) User return value of authenticate method with valid password should When you call a matcher in an example without a String, like this:
specify { expect(object).to matcher }
or this:
it { is_expected.to matcher }
RSpec expects the matcher to have a #description method. You should either
add a String to the example this matcher is being used in, or give it a
description method. Then you won't have to suffer this lengthy warning again.
Failure/Error: it { should == found_user.authenticate(#user.password) }
NoMethodError:
undefined method `authenticate' for nil:NilClass
# ./spec/models/user_spec.rb:64:in `block (4 levels) in <top (required)>'
3) User return value of authenticate method with invalid password
Failure/Error: let(:user_for_invalid_password) { found_user.authenticate("invalid") }
NoMethodError:
undefined method `authenticate' for nil:NilClass
# ./spec/models/user_spec.rb:68:in `block (4 levels) in <top (required)>'
# ./spec/models/user_spec.rb:71:in `block (4 levels) in <top (required)>'
4) User return value of authenticate method with invalid password should not When you call a matcher in an example without a String, like this:
specify { expect(object).to matcher }
or this:
it { is_expected.to matcher }
RSpec expects the matcher to have a #description method. You should either
add a String to the example this matcher is being used in, or give it a
description method. Then you won't have to suffer this lengthy warning again.
Failure/Error: let(:user_for_invalid_password) { found_user.authenticate("invalid") }
NoMethodError:
undefined method `authenticate' for nil:NilClass
# ./spec/models/user_spec.rb:68:in `block (4 levels) in <top (required)>'
# ./spec/models/user_spec.rb:70:in `block (4 levels) in <top (required)>'
5) User when email format is valid should be valid
Failure/Error: #user.should be_valid
expected #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil, password_digest: nil> to be valid, but got errors: Password can't be blank
# ./spec/models/user_spec.rb:96:in `block (4 levels) in <top (required)>'
# ./spec/models/user_spec.rb:94:in `each'
# ./spec/models/user_spec.rb:94:in `block (3 levels) in <top (required)>'
Deprecation Warnings:
Using `should` from rspec-expectations' old `:should` syntax without explicitly enabling the syntax is deprecated. Use the new `:expect` syntax or explicitly enable `:should` with `config.expect_with(:rspec) { |c| c.syntax = :should }` instead. Called from /home/user/rails_projects/sample_app/spec/models/user_spec.rb:96:in `block (4 levels) in <top (required)>'.
If you need more of the backtrace for any of these deprecations to
identify where to make the necessary changes, you can configure
`config.raise_errors_for_deprecations!`, and it will turn the
deprecation warnings into errors, giving you the full backtrace.
1 deprecation warning total
Finished in 0.49086 seconds (files took 0.54848 seconds to load)
32 examples, 5 failures
Failed examples:
rspec ./spec/models/user_spec.rb:27 # User should be valid
rspec ./spec/models/user_spec.rb:64 # User return value of authenticate method with valid password should When you call a matcher in an example without a String, like this:
specify { expect(object).to matcher }
or this:
it { is_expected.to matcher }
RSpec expects the matcher to have a #description method. You should either
add a String to the example this matcher is being used in, or give it a
description method. Then you won't have to suffer this lengthy warning again.
rspec ./spec/models/user_spec.rb:71 # User return value of authenticate method with invalid password
rspec ./spec/models/user_spec.rb:70 # User return value of authenticate method with invalid password should not When you call a matcher in an example without a String, like this:
specify { expect(object).to matcher }
or this:
it { is_expected.to matcher }
RSpec expects the matcher to have a #description method. You should either
add a String to the example this matcher is being used in, or give it a
description method. Then you won't have to suffer this lengthy warning again.
rspec ./spec/models/user_spec.rb:92 # User when email format is valid should be valid
Randomized with seed 55216
Project
sample_app/tree/modelling_user
Files
spec/models/user_spec.rb
app/models/user.rb
Gemfile
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 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.
Here is the code
#add email of team lead of the current user
def add_the_tl_email(to_address, session_user_id)
ul = UserLevel.find_by_user_id(session_user_id)
if !ul.team.nil? && !ul.role.nil?
User.user_status('active').joins(:user_levels).where(:user_levels => {:position => 'tl',
:role => ul.role,
:team => ul.team }).each do
|u| to_address << u.email
end
end
end
The error from spec:
1) CustomersController GET customer page 'create' successful should create one customer record
Failure/Error: post 'create', :customer => #customer
NoMethodError:
undefined method `team' for nil:NilClass
# ./app/mailers/user_mailer.rb:36:in `add_the_tl_email'
# ./app/mailers/user_mailer.rb:15:in `notify_tl_dh_ch_ceo'
# ./app/controllers/customers_controller.rb:27:in `create'
# ./spec/controllers/customers_controller_spec.rb:48:in `block (5 levels) in <top (required)>'
# ./spec/controllers/customers_controller_spec.rb:47:in `block (4 levels) in <top (required)>'
team is a field in user_level. Here is the user_level in factory:
Factory.define :user_level do |level|
level.role "sales"
level.position "member"
level.team 1
level.association :user
end
In rails console, the ul.team.nil? returns either true or false on test data generated by factory.
Since team is a field in user_level, why there is error in spec like: NoMethodError:
undefined method `team' for nil:NilClass ?
Thanks.
The issues is that nil.team is not a method*
NoMethodError:
undefined method `team' for nil:NilClass
That is, ul evaluates to nil: check ul.nil? before checking ul.team.nil?... but might want to find out why the find failing to start with ;-)
Happy coding.
*This is what the real error message says, the title of the post is garbage :-/
The call in
ul = UserLevel.find_by_user_id(session_user_id)
returned nil and thus nil was assigned to ul.
So when you called
!ul.team.nil?
Ruby tried to call team on nil. nil is an instance of NilClass and there is no method named team on NilClass.