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.
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
Im doing an assignment for a class and it uses column_types method in a rspec test.
it "User database structure in place" do
expect(User.column_names).to include "password_digest", "username"
expect(User.column_types["username"].type).to eq :string
expect(User.column_types["password_digest"].type).to eq :string
expect(User.column_types["created_at"].type).to eq :datetime
expect(User.column_types["updated_at"].type).to eq :datetime
end
Error: when i run rpsec in the command line.
Rails 5.0
Ubuntu 14.10
Failure/Error: expect(User.column_types["username"].type).to eq :string
NoMethodError:
undefined method `column_types' for #<Class:0x000000053a0188>
Did you mean? columns
column_names
# ./spec/assignment_spec.rb:67:in `block (5 levels) in <top (required)>'
# ./spec/assignment_spec.rb:14:in `block (2 levels) in <top (required)>'
The method was removed in this commit. It's not so easy to find it.
But, the reason is was not documented, it's because the method itself is not documented (Maybe it's just for internal use).
See this comment :nodoc: on the method when it existed:
def column_types # :nodoc:
#column_types ||= columns_hash.transform_values(&:cast_type).tap do |h|
h.default = Type::Value.new
end
end
You can read through the commit's description to understand the why and maybe see if there's something else you can do.
EDIT
Take a look at these lines maybe attributes_types or columns_hash can solve your problem.
The method column_types is removed in Rails 5.
To get the type of column you can try following code:
User.column_for_attribute('username').type
This will return the type, here in your case: :string
looks like in rails 5, column_types method no longer exist
You can use this to get a hash of all the column_types
User.columns_hash.each_with_object({}) { |obj, h| h[obj[1].name] = obj[1].sql_type }
I'm trying to make my tests robust and really solid, and I've been breaking down some complex queries and associations into smaller ones, or refactoring and moving the data into scopes.
Given the following classes:
class Item < ActiveRecord::Base
belongs_to :location
scope :in_location, ->(location) { where(location: location) }
scope :findable, ->(location, not_ids) {
in_location(location).where.not(id: not_ids)
}
end
class Container < ActiveRecord::Base
belongs_to :location
# THIS IS WHAT I WANT TO TEST
has_many :findable_items, ->(container) {
findable(container.location, container.not_findable_ids)
}, class_name: 'Item'
end
How would you test a variable has_many relationship like this without hitting the database to a significant degree? I know I can test the Item.findable method on it's own; what I'm interested in is the container.findable_items method.
Note: the actual association being tested is more complex than this, and would require pretty extensive set-up; it's running through a few other nested associations and scopes. I'd like to avoid this setup if possible, and just test that the scope is called with the correct values.
Relevant parts of my Gemfile:
rails (4.2.3)
shoulda-matchers (2.6.2)
factory_girl (4.5.0)
factory_girl_rails (4.5.0)
rspec-core (3.3.2)
rspec-expectations (3.3.1)
rspec-its (1.2.0)
rspec-mocks (3.3.2)
rspec-rails (3.3.3)
I have shoulda-matchers in my project, so I can do the basic sanity test:
it { should have_many(:findable_items).class_name('Item') }
but this fails:
describe 'findable_line_items' do
let(:container) { #container } # where container is a valid but unsaved Container
let(:location) { #container.location }
it 'gets items that are in the location and not excluded' do
container.not_findable_ids = [1,2]
# so it doesn't hit the database
expect(Item).to receive(:findable).with(location, container.not_findable_ids)
container.findable_items
end
end
This spec fails with the following error:
1) Container findable_line_items gets items that are in the location and not excluded
Failure/Error: container.findable_items
NoMethodError:
undefined method `except' for nil:NilClass
# /[redacted]/gems/activerecord-4.2.3/lib/active_record/associations/association_scope.rb:158:in `block (2 levels) in add_constraints'
# /[redacted]/gems/activerecord-4.2.3/lib/active_record/associations/association_scope.rb:154:in `each'
# /[redacted]/gems/activerecord-4.2.3/lib/active_record/associations/association_scope.rb:154:in `block in add_constraints'
# /[redacted]/gems/activerecord-4.2.3/lib/active_record/associations/association_scope.rb:141:in `each'
# /[redacted]/gems/activerecord-4.2.3/lib/active_record/associations/association_scope.rb:141:in `each_with_index'
# /[redacted]/activerecord-4.2.3/lib/active_record/associations/association_scope.rb:141:in `add_constraints'
# /[redacted]/activerecord-4.2.3/lib/active_record/associations/association_scope.rb:39:in `scope'
# /[redacted]/gems/activerecord-4.2.3/lib/active_record/associations/association_scope.rb:5:in `scope'
# /[redacted]/gems/activerecord-4.2.3/lib/active_record/associations/association.rb:97:in `association_scope'
# /[redacted]/gems/activerecord-4.2.3/lib/active_record/associations/association.rb:86:in `scope'
# /[redacted]/gems/activerecord-4.2.3/lib/active_record/associations/collection_association.rb:423:in `scope'
# /[redacted]/gems/activerecord-4.2.3/lib/active_record/associations/collection_proxy.rb:37:in `initialize'
# /[redacted]/gems/activerecord-4.2.3/lib/active_record/relation/delegation.rb:106:in `new'
# /[redacted]/gems/activerecord-4.2.3/lib/active_record/relation/delegation.rb:106:in `create'
# /[redacted]/gems/activerecord-4.2.3/lib/active_record/associations/collection_association.rb:39:in `reader'
# /[redacted]/gems/activerecord-4.2.3/lib/active_record/associations/builder/association.rb:115:in `pickable_items'
# ./spec/models/container_spec.rb:25:in `block (3 levels) in <top (required)>'
How would you get this spec to pass, without actually setting up an Item that meets all the requirements?
I ended up going with a solution like this:
describe 'findable_line_items' do
let(:container) { #container } # where container is a valid but unsaved Container
let(:location) { #container.location }
it 'gets items that are in the location and not excluded' do
# so it doesn't hit the database
expect(Item).to receive(:findable).with(location, container.not_findable_ids).and_call_original
expect(container).to receive(:location).and_call_original
expect(container).to receive(:not_findable_ids).and_call_original
container.findable_items
end
end
The error that was occurring was somewhere in the ActiveRecord association setup; it was trying to instantiate an ActiveRecord array on a nil object which was being returned from my Item stub. Adding .and_call_original solved that error.
I don't really care to test that the correct objects are being returned from this association, since that scope is being tested elsewhere, just that the scope is being used. It still hits the database in this scenario, but not the 15 times that would be required to set up a full test.
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.
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.