Short way for tests validations in Rspec - ruby-on-rails

I want a simple way to test my validations. My test focus are integrations, not validations, and I don't working with TDD. That being said, I want to replace:
# Estate model specs
describe "#name" do
it "is required" do
estate.name = nil
estate.valid?
expect(estate.errors).to have_key(:name)
end
end
describe "#company" do
it "is required" do
estate.company = nil
estate.valid?
expect(estate.errors).to have_key(:company)
end
end
# and so on..
to some like:
# Estate model specs
requiredFields = [:name, :company, :price, :region, :regions, :typologies, :image]
requiredFields.each do |requiredField|
describe "##{requiredField}" do
it "is required" do
estate[requiredField] = nil
estate.valid?
expect(estate.errors).to have_key(requiredField)
end
end
end
The fields name and price works, the problem is in associations:
Estate
is an instance of Estate
validations
with required fields
should be valid
#name
is required
#company
is required (FAILED - 1)
#price
is required
#region
is required (FAILED - 2)
...
I think that problem is estate[requiredField]. If I change to company_id will work. How can I do something like estate.requiredField in foreach?

You can perform estate.requiredField using Ruby's send method, which invokes its argument as a method on the receiver:
estate.send(requiredField)
Since you're assigning, you'll need to interpolate an = into the field name. The foo= method takes an argument, which you pass as continued arguments to send after the method name:
estate.send("#{requiredField}=", nil)`
Something to be aware of is that since send ends up calling the requested method from within the receiver, you might end up bypassing protected or private methods.
class Dog
private
def bark
puts 'Woof'
end
end
Dog.new.bark
# NoMethodError: private method 'bark' called for #<Dog:0x007fd28bad7590>
Dog.new.send :bark
# Woof

Try to use object.send
estate.send("#{requiredField}=", value) # setter, estate.requiredField=value
estate.send(requiredField) # getter, estate.requiredField
Or use https://github.com/thoughtbot/shoulda-matchers for such kind of tests

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

Rails 5 - RSpec with FactoryGirl giving error wrong number of arguments

This is the first time I am writing test cases on a rails project which is using RSpec and FactoryGirl
When I run the test case i get the following error
wrong number of arguments (given 0, expected 1)
I have gone through other posts at stack over flow and they are not much helpful in my case.
What I have tried
I am writing a test case on a Model which is called ImportFeed and it looks something like as following
class ImportFeed < ApplicationRecord
belongs_to :staffroom
belongs_to :user, optional: true # We don't have to have a user
validates_presence_of :url, :feed_type
validates :enabled, presence: true, allow_blank: true
def initialize(params)
super(params)
self.enabled = false if self.enabled.blank?
self.default_radius = DEFAULT_RADIUS if self.default_radius.blank?
self.default_days = DAYS_DEFAULT if self.default_days.blank?
end
end
This is what my test case looks like
require 'rails_helper'
describe JobImporters::JoraJobImporter, '.run' do
it 'should create an instance of ImportFeed' do
feed = ImportFeed::new FactoryGirl.create(:import_feed, :import1)
expect(feed).to be_a ImportFeed
end
end
This is the factory
FactoryGirl.define do
factory :import_feed do
trait :import1 do
enabled true
feed_type 'example'
staffroom_id 7526
url Faker::Internet::url
end
end
end
When I run this I get the error mentioned at the beginning of this question,
If I pass the data to the test case without FactoryGirl then my test case works and passes for example if I replace
feed = ImportFeed::new FactoryGirl.create(:import_feed, :import1)
with
feed = ImportFeed::new enabled: true, staffroom_id: 7526, feed_type: 'example', url: Faker::Internet::url
the test case passes.
I will really appreciate if someone can point to me what am I doing wrong here.
Because you're overriding initialize method, so you got unexpected exception.
Don't override initialize on ActiveRecord objects
ActiveRecord::Base doesn't always use new to create objects, so initialize might not be called. [link]
In order to solve your problem, you should set your attributes in callback instead
class ImportFeed < ApplicationRecord
# ...
after_initialize :set_my_attributes
private
def set_my_attributes
self.enabled = false if self.enabled.blank?
self.default_radius = DEFAULT_RADIUS if self.default_radius.blank?
self.default_days = DAYS_DEFAULT if self.default_days.blank?
end
end
One more thing:
You're testing creating an instance of ImportFeed functionality, so you should either pass params to new or create methods to test it, but you pass an instance of ImportFeed to it (from FactoryGirl).
According to the docs, ActiveRecord#new accepts Hash only (the default argument is {} if you don't pass anything).
If you pass an object to it, you'll get ArgumentError exception along with "When assigning attributes, you must pass a hash as an argument" message
def assign_attributes(new_attributes)
if !new_attributes.respond_to?(:stringify_keys)
raise ArgumentError, "When assigning attributes, you must pass a hash as an argument."
end
return if new_attributes.empty?
attributes = new_attributes.stringify_keys
_assign_attributes(sanitize_for_mass_assignment(attributes))
end
I think you are just using the initialize method to change the values according to the conditions:
def initialize(params)
super(params)
self.enabled = false if self.enabled.blank?
self.default_radius = DEFAULT_RADIUS if self.default_radius.blank?
self.default_days = DAYS_DEFAULT if self.default_days.blank?
end
You should not override it (my suggestion) as it may break many things. So instead you may change the values on a callback (before_validation, before_save, before_create, after_initialize whichever suits you) like this:
before_create :set_default_radius, if: proc { |feed| feed.default_radius.blank? }
def set_default_radius
self.default_radius = DEFAULT_RADIUS
end
And the best way to do this is having the default value in database itself. You can define that in migration:
def up
change_column :import_feeds, :default_radius, :integer, default: 0
end
def down
change_column :import_feeds, :default_radius, :integer, default: nil
end
So if the value is not defined it will always set to the default value mentioned in the migration file.
Also you may have a read of several question related to this which has some very good answers and explanation:
How to override "new" method for a rails model
Why is overriding ActiveRecord::Base.initialize wrong?
Overriding ApplicationRecord initialize, bad idea?

rails class instead of switch

I have been looking for over an hour on Internet and I can't find anything about this.
I am creating filters for data of a website and currently these are handled by a case statement
class MyClass
attr_accessor :attribute
def self.function(value)
query = case value
when "open" then "Open"
...
end
where(:attribute => query)
end
end
Because of various reasons (i.e. dynamic instead of hard coding the filters) I want to create a model out of this with a getter and setter, but I can't get this to work
My new function:
def self.function(value)
Attribute.name = value
where(:attribute => Attribute.name)
end
My new model:
class Attribute
attr_accessor :name
end
And the test:
it "should set the attribute to 'hello'" do
MyClass.function("hello")
Attribute.name.should eql "hello"
end
gives an error:
Failure/Error: Myclass.function("hallo")
NoMethodError:
undefined method `name=' for Attribute:Class
Any help would be appreciated
This is because the attr_accessor is defining instance method (ie: method that works on an instance of Attribute) and you try to use it as class method (ie: Attribute.name).
You may rewrite your function this way :
def self.function(value)
attribute = Attribute.new
attribute.name = value
where(:attribute => attribute.name)
end

Creating new Ruby string helper without argument params

I really had a hard time figuring out how to word this question, but in essence, I want to do this:
model = MyModel.new
model.title = "foo bar"
model.title.to_id #=> "foo_bar"
I have an ActiveRecord class for MyModel
class MyModel < ActiveRecord::Base
def to_id(str)
str.downcase.gsub(" ", "_")
end
end
but, of course, it's looking for the to_id method on String, and I don't want to override string, because I don't require this behaviour on every string. Just strings associated with MyModel. I could keep it simple and do something like:
model.to_id(model.title)
But that's not very Ruby.
I know I've seen examples of this sort of method implemented before, I just can't track them down.
Halp anyone?
you can extend a specific object instance with a method, using modules.
module ToId
def to_id
self.downcase.gsub " ", "_"
end
end
class MyClass
def title=(value)
value.extend ToId
#title = value
end
def title
#title
end
end
m = MyClass.new
m.title = "foo bar"
puts m.title #=> foo bar
puts m.title.to_id #=> foo_bar
since the value passed into the .title= method is a string, when we extend the string string with the ToId module, "self" in the module's methods is a string. therefore, we have direct access to the string that was passed into the .title= method, and we can manipulate it directly.
this is all done without having to modify the String class directly. we are only extending the specific instance that represents the title.
I believe that true Ruby solution is based on meta-programming. I'd strongly recommend you this book http://pragprog.com/titles/ppmetr/metaprogramming-ruby ($20) if you are interested.
By the way - the solution proposed above probably will not work as overriding column accessors is not that simple.
So I would recommend to create a class method that you use in your model definition like this:
class MyModel < ActiveRecord::Base
# adds to_id to the following attributes
ideize :name, :title
end
Well, that was an easy part, now comes the tougher one - the module itself:
#
# extends the ActiveRecord with the class method ideize that
# adds to_id method to selected attributes
#
module Ideizer
module ClassMethods
def ideize(*args)
# generates accessors
args.each do |name|
define_method("#{name}") do
# read the original value
value = read_attribute(name)
# if value does not contain to_id method then add it
unless value.respond_to?(:to_id)
# use eigen class for the value
class << value
def to_id
self.downcase.gsub " ", "_"
end
end
end
# return the original value
value
end
end
end
end
def self.included(base)
base.extend(ClassMethods)
end
end
# extend the active record to include ideize method
ActiveRecord::Base.send(:include, Ideizer)
I have to admit that I did not write the solution above just from my memory so I've prepared some tests that I'm sharing here:
require 'spec_helper'
describe MyModel do
before :each do
#mod = MyModel.new(:name => "Foo Bar",
:title => "Bar Bar",
:untouched => "Dont touch me")
end
it "should have to_id on name" do
#mod.name.respond_to?(:to_id).should be_true
#mod.name.to_id.should eql "foo_bar"
end
it "should have to_id on title" do
#mod.title.respond_to?(:to_id).should be_true
#mod.title.to_id.should eql "bar_bar"
end
it "should NOT have to_id on untouched" do
#mod.untouched.respond_to?(:to_id).should be_false
end
it "should work with real model" do
#mod.save!
#mod.name.to_id.should eql "foo_bar"
# reload from the database
#mod.reload
#mod.name.to_id.should eql "foo_bar"
end
end
Ruby rules!
You should take a look at the functions within ActiveSupport::CoreExtensions::String::Inflections, specifically in your case I would use the underscore method that exist in the (expanded) String class.
Then to override the accessor you could do something like:
def title_id
read_attribute(:title).underscore
end
I think that's what you want.

Is there a way to validate a specific attribute on an ActiveRecord without instantiating an object first?

For example, if I have a user model and I need to validate login only (which can happen when validating a form via ajax), it would be great if I use the same model validations defined in the User model without actually instantiating a User instance.
So in the controller I'd be able to write code like
User.valid_attribute?(:login, "login value")
Is there anyway I can do this?
Since validations operate on instances (and they use the errors attribute of an instance as a container for error messages), you can't use them without having the object instantiated. Having said that, you can hide this needed behaviour into a class method:
class User < ActiveRecord::Base
def self.valid_attribute?(attr, value)
mock = self.new(attr => value)
unless mock.valid?
return mock.errors.has_key?(attr)
end
true
end
end
Now, you can call
User.valid_attribute?(:login, "login value")
just as you intended.
(Ideally, you'd include that class method directly into the ActiveRecord::Base so it would be available to every model.)
Thank you Milan for your suggestion. Inspired by it I created a simple module one can use to add this functionality to any class. Note that the original Milans suggestion has a logic error as line:
return mock.errors.has_key?(attr)
should clearly be:
return (not mock.errors.has_key?(attr))
I've tested my solution and it should work, but ofc I give no guarantees. And here's my glorious solution. Basically a 2-liner if you take away the module stuff.. It accepts method names as stings or symbols.
module SingleAttributeValidation
def self.included(klass)
klass.extend(ClassMethods)
end
module ClassMethods
def valid_attribute?(attr, value)
mock = self.new(attr => value)
(not mock.valid?) && (not mock.errors.has_key?(attr.class == Symbol ? attr : attr.to_sym))
end
end
end
To use your standard validation routines:
User.new(:login => 'login_value').valid?
If that does not work for you, build a custom class method for this:
class User < ActiveRecord::Base
validate do |user|
user.errors.add('existing') unless User.valid_login?(user.login)
end
def self.valid_login?(login)
# your validation here
!User.exist?(:login=> login)
end
end
I had a hell of a time getting this to work in Rails 3.1. This finally worked. (Not sure if it's the best way to do it, I'm kind of a newb.). The problem I was having was that value was being set to type ActiveSupport::SafeBuffer, and was failing validation.
def self.valid_attribute?(attr, value)
mock = User.new(attr => "#{value}") # Rails3 SafeBuffer messes up validation
unless mock.valid?
return (not mock.errors.messages.has_key?(attr))
end
return true
end
I have gone with the custom class solution but I just wanted to make sure there was no better way
class ModelValidator
def self.validate_atrribute(klass, attribute, value)
obj = Klass.new
obj.send("#{attribute}=", value)
obj.valid?
errors = obj.errors.on(attribute).to_a
return (errors.length > 0), errors
end
end
and I can use it like
valid, errors = ModelValidator.validate_attribute(User, "login", "humanzz")
class User < ActiveRecord::Base
validates_each :login do |record, attr, value|
record.errors.add attr, 'error message here' unless User.valid_login?(value)
end
def self.valid_login?(login)
# do validation
end
end
Just call User.valid_login?(login) to see if login itself is valid
An implementation of the 'valid_attribute' method you are suggesting:
class ActiveRecord:Base
def self.valid_attribute?(attribute, value)
instance = new
instance[attribute] = value
instance.valid?
list_of_errors = instance.errors.instance_variable_get('#errors')[attribute]
list_of_errors && list_of_errors.size == 0
end
end
How about:
User.columns_hash.has_key?('login')

Resources