Rails - Test validation of enum fields - ruby-on-rails

I'm using Rails 4 enums and I want to properly test them, so I set these tests up for my enum fields:
it { should validate_inclusion_of(:category).in_array(%w[sale sale_with_tax fees lease tax_free other payroll]) }
it { should validate_inclusion_of(:type).in_array(%w[receivable payable]) }
And this is the model they're validating:
class Invoice < ActiveRecord::Base
belongs_to :user
enum category: [:sale, :sale_with_tax, :fees, :lease, :tax_free, :other, :payroll]
enum type: [:receivable, :payable]
validates :user, presence: true
validates :issue_date, presence: true
validates :series, presence: true
validates :folio, presence: true
validates :issuing_location, presence: true
validates :payment_method, presence: true
validates :last_digits, presence: true
validates :credit_note, presence: true
validates :total, presence: true
validates :subtotal, presence: true
validates :category, presence: true
validates_inclusion_of :category, in: Invoice.categories.keys
validates :type, presence: true
validates_inclusion_of :type, in: Invoice.types.keys
end
But when I run the tests I get:
1) Invoice should ensure inclusion of type in [0, 1]
Failure/Error: it { should validate_inclusion_of(:type).in_array([0,1]) }
ArgumentError:
'123456789' is not a valid type
# ./spec/models/invoice_spec.rb:20:in `block (2 levels) in <top (required)>'
2) Invoice should ensure inclusion of category in [0, 1, 2, 3, 4, 5, 6]
Failure/Error: it { should validate_inclusion_of(:category).in_array([0,1,2,3,4,5,6]) }
ArgumentError:
'123456789' is not a valid category
# ./spec/models/invoice_spec.rb:19:in `block (2 levels) in <top (required)>'
I've also tried with string values in the test arrays, but I get the same error and I really don't understand it.

Using Shoulda matchers we can use the following to test the enum
it { should define_enum_for(:type).with([:receivable, :payable]) }
it { should define_enum_for(:category).
with(:sale, :sale_with_tax, :fees, :lease, :tax_free, :other, :payroll) }

Try this:
it { should validate_inclusion_of(:category).in_array(%w[sale sale_with_tax fees lease tax_free other payroll].map(&:to_sym)) }
Additionally, for code-cleanup, try putting the valid categories/types in a corresponding constant. Example:
class Invoice < ActiveRecord::Base
INVOICE_CATEGORIES = [:sale, :sale_with_tax, :fees, :lease, :tax_free, :other, :payroll]
enum category: INVOICE_CATEGORIES
end

Your migration could be the issue, it should look something like:
t.integer :type, default: 1
You may also consider testing this another way.
Maybe more like:
it "validates the category" do
expect(invoice with category fee).to be_valid
end

Use shoulda matchers along with check for column_type.
it do
should define_enum_for(:type).
with_values(:receivable, :payable).
backed_by_column_of_type(:integer)
end
it do
should define_enum_for(:category).
with_values(:sale, :sale_with_tax, :fees, :lease, :tax_free, :other, :payroll).
backed_by_column_of_type(:integer)
end

Just use shoulda matchers:
it { should define_enum_for(:type).with_values([:receivable, :payable]) }
it { should define_enum_for(:category).with_values(:sale, :sale_with_tax, :fees, :lease, :tax_free, :other, :payroll)}

You have this string in your validations:
validates_inclusion_of :category, in: Invoice.categories.keys
In case of enum
Invoice.categories.keys #=> ["sale", "sale_with_tax", "fees", "lease", "tax_free", "other", "payroll"]
You should update your object data with one of names of your enum.

Related

undefined method `user' for #<Class:0x00007ffbd1c309b8>

I don't understand why I can't use self here?
class PayoutRequest < ApplicationRecord
validates :phone, confirmation: true, on: :create
validates :phone_confirmation, presence: true, on: :create
belongs_to :user, foreign_key: "user_id"
validates :amount, numericality: { only_integer: true, greater_than_or_equal_to: 300, smaller_than_or_equal: self.user.balance }
scope :paid, -> { where(:paid => true) }
scope :unpaid, -> { where(:paid => false) }
end
How can I write this?
Use a custom method, for example:
validate :amount_not_greater_than_balance
def amount_not_greater_than_balance
return if amount <= user.balance
errors.add(:amount, "can't be greater than balance")
end
In addition, you should probably only run this specific validation rule on: :create -- because it would presumably be totally acceptable for a payment request to become more than the user's balance, later on n the future.
Because self is not what you think it is. In case you didn't know or forgot, validation DSL is just methods called on the class itself. Here you basically call PayoutRequest.validates and pass it some parameters.
validates :amount, numericality: { only_integer: true, greater_than_or_equal_to: 300, smaller_than_or_equal: self.user.balance }
^ ^ arg ^ kw arg ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
method name just a regular hash, defined at the class level. So `self` is the class.

Model's validation test does not work in rspec

I wanted to see if the validation was working properly, so I wrote a test of the model in Rspec.
models/work_history.rb
class WorkHistory < ApplicationRecord
belongs_to :account
validates :name,
:since_date,
:position, presence: true
validates :is_employed, inclusion: [true, false]
validates :until_date, presence: true, if: -> { :is_employed == false }
end
factories/work_histories.rb
FactoryBot.define do
factory :work_history do
sequence(:name) { |n| "#{n}_company" }
since_date { '2017-04-01' }
until_date { '2021-03-01' }
is_employed { false }
position { 'director' }
department { 'sales' }
association :account
end
end
spec/models/work_history_spec.rb
RSpec.describe WorkHistory, type: :model do
let(:account) { create(:account) }
let(:work_history) { build(:work_history, account: account) }
it "is invalid when is_employed is false and until_date is nil" do
work_history.is_employed = false
work_history.until_date = ''
expect(work_history.valid?).to eq(false)
end
it "is invalid when is_employed is employed" do
work_history.is_employed = 'employed'
expect(work_history.valid?).to eq(false)
end
end
then I run rspec command, but the test did't pass.
this is the rspec test error log
docker-compose run app rspec spec/models
Creating toei-works_app_run ... done
FF
Failures:
1) WorkHistory is invalid when is_employed is false and until_date is nil
Failure/Error: expect(work_history.valid?).to eq(false)
expected: false
got: true
(compared using ==)
Diff:
## -1 +1 ##
-false
+true
# ./spec/models/work_history_spec.rb:10:in `block (2 levels) in <top (required)>'
2) WorkHistory is invalid when is_employed is employed
Failure/Error: expect(work_history.valid?).to eq(false)
expected: false
got: true
(compared using ==)
Diff:
## -1 +1 ##
-false
+true
# ./spec/models/work_history_spec.rb:15:in `block (2 levels) in <top (required)>'
I don't know why Rspec tests fail
Why can't I pass rspec's test?
You're not structuring the if part of your validation correctly. You're currently checking whether the symbol :is_employed? equals false, which it never will. You can check out the correct syntax here:
https://guides.rubyonrails.org/active_record_validations.html#conditional-validation
You can either do
class WorkHistory < ApplicationRecord
belongs_to :account
validates :name,
:since_date,
:position, presence: true
validates :is_employed, inclusion: [true, false]
validates :until_date, presence: true, if: -> { :not_employed? }
private
def not_enployed?
!is_employed?
end
end
or
class WorkHistory < ApplicationRecord
belongs_to :account
validates :name,
:since_date,
:position, presence: true
validates :is_employed, inclusion: [true, false]
validates :until_date, presence: true, unless: Proc.new { |work_history| work_history.is_employed? }
end

Validate uniqueness of globalized field

I have a model with translated fields using the globalize gem and globalize-accessors gem for providing localized attributes such as name_en, name_zh_hk for a localized name field.
for example:
class Person < ActiveRecord::Base
translates :name
globalize_accessors: locales: [:en, :"zh-HK"], attributes: [:name]
# problem is:
validates :name, presence: true, uniqueness: true
end
So now name_en and name_zh_hk can get and set the value in corresponding locale correctly.
However, the validates :name validates only the name field in Person model. I also want to validate the uniqueness of the chinese input.
In short, would like a (easy) way to validate uniqueness of both name_en and name_zh_hk
** I have a form to submit both name_en and name_hk.
At the end of your person.rb model file (outside of class Person ... end, add this:
Person::Translation.class_eval do
validates_presence_of :name
validates_uniqueness_of :name
end
You have to do this
class Person < ActiveRecord::Base
translates :name
class Translation
validates :name, presence: true, uniqueness: true
end
end
I could be confused what you are asking about unique scopes:
validates :name, uniqueness: {scope: :blah}
specifically you may want to have a "PersonName" model.
PersonName
name | local | person_id
Person has_many :names
then have:
validates :name, uniqueness: { scope: :person_id }
this way if they enter a name for HK the same as the name for :en it will not be valid.
Solved with the following code.
Model
# /app/models/category.rb
...
I18n.available_locales.each do |locale|
validates :"name_#{locale}", presence: true, length: { maximum: 5 }, uniqueness: true
end
Validator
# config/initializers/associated_translations_uniqueness_validator.rb
require 'active_record'
require 'active_record/validations/uniqueness.rb'
ActiveRecord::Validations::UniquenessValidator.class_eval do
def validate_each_with_associated_translations(record, attribute, value)
klass = record.class
if klass.translates? && !klass.translated?(attribute) && klass.globalize_attribute_names.include?(attribute)
attribute_parts = attribute.to_s.rpartition('_')
raw_attribute = attribute_parts.first.to_sym
locale = attribute_parts.last.to_sym
finder_class = klass.translation_class
table = finder_class.arel_table
relation = build_relation(finder_class, table, raw_attribute, value).and(table[:locale].eq(locale))
relation = relation.and(table[klass.reflect_on_association(:translations).foreign_key].not_eq(record.send(:id))) if record.persisted?
translated_scopes = Array(options[:scope]) & klass.translated_attribute_names
untranslated_scopes = Array(options[:scope]) - translated_scopes
untranslated_scopes.each do |scope_item|
scope_value = record.send(scope_item)
reflection = klass.reflect_on_association(scope_item)
if reflection
scope_value = record.send(reflection.foreign_key)
scope_item = reflection.foreign_key
end
relation = relation.and(find_finder_class_for(record).arel_table[scope_item].eq(scope_value))
end
translated_scopes.each do |scope_item|
scope_value = record.send(scope_item)
relation = relation.and(table[scope_item].eq(scope_value))
end
if klass.unscoped.with_translations.where(relation).exists?
record.errors.add(attribute, :taken, options.except(:case_sensitive, :scope).merge(:value => value))
end
else
validate_each_without_associated_translations(record, attribute, value)
end
end
alias_method_chain :validate_each, :associated_translations
end

Rspec adds extra default error

I have the following model :
class Court < ActiveRecord::Base
#Relationships
#belongs_to :case, class_name: 'Case', foreign_key: 'case_id'
belongs_to :user, class_name: 'User', foreign_key: 'user_id'
#Scopes
#Attributes
attr_accessible :court_name, :court_notes, :street, :city, :state, :zip
#Validations
validates_lengths_from_database
validates :court_name, presence: true, length: { in: 3..200 }
validates :court_notes, length: { maximum: 250 }
validates :court_notes, :street, :city, :state, :zip, presence: true
validates :street, :city, :state, length: { maximum: 30, message: 'max length allowed is 30' }
validates :zip, numericality: true, length: { is: 5, message: 'length should be 5' }, allow_blank: true
#Callbacks
#Methods
end
And the following spec file :
require 'spec_helper'
describe Court do
context '#object' do
it 'has a valid factory' do
FactoryGirl.build(:court).should be_valid
end
end
context '#associations' do
it { should belong_to(:user) }
end
context '#values' do
it { should respond_to(:court_name) }
it { should respond_to(:court_notes) }
it { should respond_to(:street) }
it { should respond_to(:city) }
it { should respond_to(:state) }
it { should respond_to(:zip) }
end
context '#protected' do
it { should_not allow_mass_assignment_of(:id) }
it { should_not allow_mass_assignment_of(:case_id) }
end
context '#validations' do
it { should validate_presence_of(:court_name) }
it { should ensure_length_of(:court_name).is_at_most(200) }
it { should ensure_length_of(:court_notes).is_at_most(250) }
it { should ensure_length_of(:street).is_at_most(30) }
it { should ensure_length_of(:city).is_at_most(30) }
it { should ensure_length_of(:state).is_at_most(30) }
end
end
When I run the spec, I get the error
1) Court#validations
Failure/Error: it { should ensure_length_of(:city).is_at_most(30) }
Did not expect errors to include "is too long (maximum is 30 characters)" when city is set to "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", got error:
# ./spec/models/court_spec.rb:52:in `block (3 levels) in <top (required)>'
and two other similar errors for city and state. Where is the extra error message being added from? The validation is happening at only one place as far as I know but there are two error messages being produced.
The reason is your custom message:
message: 'max length allowed is 30'
Shoulda expects you have exact error message as default:
'is too long (maximum is 30 characters)'
But you have a different message so expectation fails. You can check Shoula doc to see how to allow custom message.

NoMethodError: undefined method `*' for nil:NilClass Factory Girl/Capybara issue

This particular test is trying to create a 'status update' for a user.
Here is the full error:
Failure/Error: FactoryGirl.create(:status_update, user: #user, created_at: 1.day.ago)
NoMethodError:
undefined method `*' for nil:NilClass
# ./app/models/status_update.rb:31:in `default_values'
# ./spec/models/user_spec.rb:28:in `block (3 levels) in <top (required)>'
Here is the test:
describe "Status Update Associations" do
before { #user.save }
let!(:older_status_update) do
FactoryGirl.create(:status_update, user: #user, created_at: 1.day.ago)
end
let!(:newer_status_update) do
FactoryGirl.create(:status_update, user: #user, created_at: 1.hour.ago )
end
it "should have status updates in the right order" do
#user.status_update.should == [newer_status_update, older_status_update]
end
end
Since the error is pointing to the status update model I might as well include that here as well. I suspect it's got something to do with some variables being set after initialization and the let! in the test, although I'm stumped with trying different callbacks.
class StatusUpdate < ActiveRecord::Base
belongs_to :user
after_initialize :default_values
attr_accessible :current_weight,
:current_bf_pct,
:current_lbm,
:current_fat_weight,
:change_in_weight,
:change_in_bf_pct,
:change_in_lbm,
:change_in_fat_weight,
:total_weight_change,
:total_bf_pct_change,
:total_lbm_change,
:total_fat_change
validates :user_id, presence: true
validates :current_bf_pct, presence: true,
numericality: true,
length: { minimum: 4, maximum:5 }
validates :current_weight, presence: true,
numericality: true,
length: { minimum: 4, maximum:5 }
validates :current_lbm, presence: true
validates :current_fat_weight, presence: true
def default_values
self.current_fat_weight = self.current_weight * self.current_bf_pct
self.current_lbm = self.current_weight - self.current_fat_weight
end
default_scope order: 'status_update.created_at DESC'
end
Here is the factory that adds the 'current_weight and current_bf_pct to the default_values method.
factory :status_update do
user
current_weight 150
current_bf_pct 0.15
end
Thanks!
It's due to your default_values method.
You're doing self.current_weight * self.current_bf_pct but none of them are set to a numerical value.

Resources