Rails validation dependent on associated object - ruby-on-rails

So I'm writing code in which we need to issue refunds. I wrote a refund validator that checks to ensure that the refund is not for more than the original charge. However, in my specs I came to the realization that the associated charge isnt' present yet. Using FactorGirl. How can I make something like this work?
Validator
class RefundValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless value <= record.charge.amount
record.errors[:attribute] << "is greater than original charge"
end
end
end
Validation
validates :amount, refund: true
Factory
FactoryGirl.define do
factory :refund do
association :client
association :charge
amount 99
end
end
Spec
context 'validations' do
%i(client_id therapist_id appointment_id booking_id value).each do |attr|
it { is_expected.to validate_presence_of attr }
end
it { is_expected.to validate_numericality_of(:value).is_greater_than_or_equal_to(0).is_less_than_or_equal_to(5) }
end
Error (i get this for all 4 attributes in the presence spec, not just client_id):
1) Refund validations should require client_id to be set
Failure/Error: it { is_expected.to validate_presence_of attr }
NoMethodError:
undefined method `amount' for nil:NilClass
# ./app/validators/refund_validator.rb:3:in `validate_each'
# ./spec/models/refund_spec.rb:20:in `block (4 levels) in <top (required)>'
I have tried explicitly creating the Factory model with a charge factory explicitly specified, but to no avail. No clue what's going on. Any help would be appreciated.

I would change unless x <= y to if x > y for the sake of readability
In any case, you should use record.try(:charge).try(:amount) instead of record.charge.amount to prevent NoMethodErrors in your validator.
Let me know if that fixes the problem already.

Related

Rails 5.2 rspec - How to test if a model is actually using a custom validator?

I have created a custom validator which has it's own specific unit tests to check that it works.
Using should-matchers there was a suggestion to add a validates_with matcher, so you could write:
subject.validates_with(:custom_validator)
Quite rightly the suggestion was declined, since it does not really test the behaviour of the model.
But my model has 4 fields that use the custom validator, and I want that behaviour to be tested - ie that those 4 fields are being validated, just as I am testing that they are being validated for presence:
describe '#attribute_name' do
it { is_expected.to validate_presence_of(:attribute_name) }
end
So how can I write a test that basically does the same thing, something sort of like this:
describe '#attribute_name' do
it { is_expected.to use_custom_validator_on(:attribute_name) }
end
This question asks the same thing and the answer suggests building a test model. However, my validator requires an option, it is used like this:
\app\models\fund.rb
class Fund < ActiveRecord
validates :ein, digits: { exactly: 9 }
end
So if I build a test model, and test it as suggested:
it 'is has correct number of digits' do
expect(build(:fund, ein: '123456789')).to be_valid
end
it 'is has incorrect number of digits' do
expect(build(:fund, ein: '123').to be_invalid
end
I receive RecordInvalid error (from my own validator! lol) saying I did not supply the required option for the validator. That option is called 'exactly'.
1) Fund#ein validates digits
Failure/Error: raise ActiveRecord::RecordInvalid # option :exactly was not provided (incorrect usage)
ActiveRecord::RecordInvalid:
Record invalid
So is Rspec not 'seeing' the value '9' defined in the model file?
Obviously it makes no sense to define that in the test as that is the defined behaviour I am trying to test for. Think of it like the validates_length_of testing for the { length: x } option.
Surely there must be a way to test that this custom validator option is set on the model?
The validator code
class DigitsValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return if value.blank?
length = options[:exactly]
regex = /\A(?!0{#{length}})\d{#{length}}\z/
return unless value.scan(regex).empty?
record.errors[attribute] << (options[:message] || error_msg(length))
end
private
def error_msg(length)
I18n.t('activerecord.errors.custom.digits_attr_invalid', length: length) if length
raise ActiveRecord::RecordInvalid # option :exactly was not provided (incorrect usage)
end
end
Interesting side note
Obviously if I remove the 'raise' line from the DigitsValidator then both the tests succeed. Is there something wrong with my code that I cannot see?
I think you would have to add a return statement, no? :-)
def error_msg(length)
return I18n.t('activerecord.errors.custom.digits_attr_invalid', length: length) if length
raise ActiveRecord::RecordInvalid # option :exactly was not provided (incorrect usage)
end
Alternatively, remove that method and use a guard after setting length:
class DigitsValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return if value.blank?
length = options[:exactly]
raise ActiveRecord::RecordInvalid if length.nil?
regex = /\A(?!0{#{length}})\d{#{length}}\z/
return unless value.scan(regex).empty?
record.errors[attribute] <<
(options[:message] ||
I18n.t('activerecord.errors.custom.digits_attr_invalid', length: length))
end
end
I think you should not aim for testing whether the model is using a specific validator. Rather check if the model is valid/invalid in specific cases. In other words, you should be able to test the behaviour of the model without knowing the implementation.
So in this case, you should setup you model correctly with you 'exactly' option for the validator and test if the model validation is sufficient overall.
On the other hand, if you are worried about that someone will misuse the validator in the future and 'exactly' is a required option for the validator, then you should raise error every time when the option is not present and test the validator in isolation like explained here: How to test a custom validator?
I like the idea of not including tests on the model that assume knowledge of exactly what the custom validator is validating. (Otherwise, we'll be repeating logic in the tests for the custom validators, and the tests for the model.)
I solved this by using Mocha (mocking library for Ruby) to set up expectations that the validate_each method of each my custom validators were being called on the correct corresponding field of my model. Simplified example:
Model class:
class User
include ActiveModel::Model
attr_accessor :first_name, :last_name
validates :first_name, first_name: true
validates :last_name, last_name: true
end
Custom validator classes:
class FirstNameValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
# ...
end
end
class LastNameValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
# ...
end
end
Model test class:
class UserTest < ActiveSupport::TestCase
def test_custom_validators_called_on_the_appropriate_fields
user = User.new(first_name: 'Valued', last_name: 'Customer')
FirstNameValidator.any_instance.expects(:validate_each).with(user, :first_name, 'Valued')
LastNameValidator.any_instance.expects(:validate_each).with(user, :last_name, 'Customer')
assert_predicate user, :valid?
end
end

Stubing a model constant for assosiation (undefined method `relation_delegate_class')

I have a Lesson model which is assosiated with Permission model:
app/models/lesson.rb:
class Lesson < ActiveRecord::Base
has_many :permissions, :class_name => 'Permission', as: :permissible, dependent: :destroy
...
def create_permissions
Permission::DEFAULTS[:lesson].each do |action, value|
..
end
end
app/models/permission.rb:
class Permission < ActiveRecord::Base
DEFAULTS = {
lesson: {some_more_action: 15}
}
belongs_to :permissible, polymorphic: true
end
I used RSpec stub_const method to stub a nested defined constant:
spec/models/lesson_spec.rb:
require 'rails_helper'
RSpec.describe Lesson, :type => :model do
describe "#create_permissions" do
let!(:lesson) { FactoryGirl.build_stubbed :lesson }
before(:each) do
stub_const('Permission::DEFAULTS', {lesson: {some_action: 5}})
end
it 'should create permissions' do
lesson.create_permissions
permission = lesson.permissions.first
...
end
end
end
But the spec fails with error:
Failure/Error: permission = lesson.permissions.first
NoMethodError:
undefined method `relation_delegate_class' for Permission:Module
# /home/install/.rvm/gems/ruby-2.1.4/gems/activerecord-4.2.3/lib/active_record/relation/delegation.rb:112:in `relation_class_for'
# /home/install/.rvm/gems/ruby-2.1.4/gems/activerecord-4.2.3/lib/active_record/relation/delegation.rb:106:in `create'
# /home/install/.rvm/gems/ruby-2.1.4/gems/activerecord-4.2.3/lib/active_record/associations/collection_association.rb:41:in `reader'
# /home/install/.rvm/gems/ruby-2.1.4/gems/activerecord-4.2.3/lib/active_record/associations/builder/association.rb:115:in `permissions'
# ./spec/models/lesson_spec.rb:285:in `block (3 levels) in <top (required)>'
It looks as if permissions are no longer rails assosiatiated with lesson model. Any ideas on how to get this round.
Gem versions: rspec-3.3.0, rspec-rails-3.3.3, rails-4.2.3.
Take a closer look to this error message:
NoMethodError:
undefined method `relation_delegate_class' for Permission:Module
Permission is described as a Module here, not as a Class. I think this happens because at the moment of stubbing nested const, Permission class has not been loaded yet, and RSpec has to stub it too via Module.
Try this as a workaround:
before(:each) do
Permission # load real ActiveRecord Permission class
stub_const('Permission::DEFAULTS', {lesson: {some_action: 5}})
end
EDIT: As a side note, I don't think that exposing nested constants to other classes is a good idea, you have little control over the constant (just changing a name or value) and you can't wrap any behavior like you do it with a method. I recommend to change public Permission's API to a using a method, it will also be easier to stub:
class Permission < ActiveRecord::Base
DEFAULTS = {
lesson: {some_more_action: 15}
}
def self.defaults
DEFAULTS
end
end
And in your spec:
before(:each) do
allow(Permission).to receive(:defaults).and_return(lesson: {some_action: 5})
end
The only option that I came upon was just to set constant explicitly in an example:
before(:each) do
Permission::DEFAULTS = {lesson: {some_action: 5}}
end
But I don't feel it's a good idea. It rises a couple of warnings also:
spec/models/lesson_spec.rb:279: warning: already initialized constant Permission::DEFAULTS
app/models/permission.rb:2: warning: previous definition of DEFAULTS was here

undefined method `valid?' testing with FactoryGirl

I'm performing the simplest test on the following class (inside model's folder):
class Offer
attr_accessor :title, :payout, :thumbnail
def initialize(title, payout, thumbnail)
#title = title
#payout = payout
#thumbnail = thumbnail
end
end
The thing is there's no 'offers' db table. The objects created out of this class are never saved in a database.
Then i perform the tests using rspec:
describe Offer do
it "has a valid factory" do
expect(FactoryGirl.create(:offer)).to be_valid
end
...
end
and FactoryGirl:
FactoryGirl.define do
factory :offer do
skip_create
title { Faker::Name.name }
payout { Faker::Number.number(2) }
thumbnail { Faker::Internet.url }
initialize_with { new(title, payout, thumbnail)}
end
end
And i get the following error:
> undefined method `valid?' for #<Offer:0x00000002b78958>
Because your Offer class is not inheriting from ActiveRecord::Base, you're not getting any of the stuff that comes along with it (such as validations). valid? is a method provided through ActiveRecord's modules, not by Ruby directly, so it won't be available on a basic Ruby class.
If all you care about is validations, then you can include the ActiveModel::Validations module in your class and it will give you valid? as well as validates_presence_of, etc.:
class Offer
include ActiveModel::Validations
...
end
You can also just include ActiveModel to get a couple other things such as ActiveRecord's naming and conversion benefits (as well as validation).

RSpec Shoulda validates_presence_of nilClass

When I use Shoulda's validates_presence_of, it stumbles on a before_validation callback.
before_validation :set_document, :set_product, :set_price
I'm trying to get this spec to pass:
it { should validate_presence_of(:quantity).with_message("Please a quantity.") }
I have database defaults of 0 for a line item's quantity, unit_price, tax_rate, and price. Before a line item is validated I compute the price from the other attributes in case they have changed.
I get this error, and similar errors, for all of the attributes involved in this computation:
3) LineItem
Failure/Error: it { should validate_presence_of(:quantity).with_message("Please a quantity.") }
NoMethodError:
undefined method `*' for nil:NilClass
# ./app/models/line_item.rb:153:in `total_price'
# ./app/models/line_item.rb:223:in `set_price'
# ./spec/models/line_item_spec.rb:32:in `block (2 levels) in <top (required)>'
My callback, set_price, is very simple:
def set_price
self.price = total_price
end
And the total_price method is very simple as well:
def total_price
quantity * unit_price * (1 + tax_rate/100)
end
I'd appreciate any help with this one as I'm completely stumped. I did see some people post about custom validation methods. This seems so basic I can't figure it out how to proceed.
Since total_price runs before validation, quantity can be nil at the time the callback is executed. This is in fact what happens behind the scenes when the Shoulda matcher runs, which is why you get an error. It's trying to send the * method to quantity, which is nil.
Use after_validation or before_save instead.

Factory Girl with serialized field

I'm having this issue with factory girl where it gives me a undefined method 'each' for #<String:0x0000012915bc18> error with a serialized field coming from the factory.
within ActiveRecord, it runs the each with no problem, as the object returned is an array.
My question is: how should I format the serialized object in my factory? The way that active record returns it? or the way it's actually stored in the database? (i.e. serialized or not?) will rspec do the same serialize magic on saving and retrieving that active record does?
this is a simplified version of what I'm doing:
Tvdb.rb-- Model
class Tvdb < ActiveRecord::Base
set_table_name 'tvdb'
serialize :cache
def self.episodes(id)
cached = self.find_by_term('episodes_' + id.to_s)
return cached.cache unless cached.nil?
info = self.series_info(id)
request = info.episodes
Tvdb.create(:term=>'episodes_' + info.id.to_s, :cache=>request)
return request
end
end
Then in my Series.rb model I can do this:
class Series < ActiveRecord::Base
def episodes
episodes = Tvdb.episodes(self.tvdb_id)
episodes.each do |episode|
puts episode.name
end
end
end
Tvdb.rb -- Factory
FactoryGirl.define do
factory :series1_episodes, :class=>Tvdb do
term 'episodes_79488'
cache %q([#<AnObject::Module:0x000001290a4568 #value="dsada"]>,#<AnObject::Module:0x0002321290a4568 #value="dsadsada"]> )
end
end
note: The syntax of the cache value might be invalid here, I tried to shorten what was a very long serialized object. The point is that it works in my model, but not in rspec
and in my *series_spec.rb* calling this:
series.episodes.count.should_not == 0
gives that error
undefined method 'each' for #<String:0x0000012915bc18>
In your factory, you shouldn't set cache to the serialized value, but to the actual value.
FactoryGirl.define do
factory :series1_episodes, :class => Tvdb do
term 'episodes_79488'
cache ["foo", "bar"]
end
end
You can change it to JSON this way:
FactoryGirl.define do
factory :series1_episodes, class: Tvdb do
term { 'episodes_79488' }
cache { %i[dsada dsadsada].to_json }
end
end

Resources