I've got one User model which has different validations based on an environmental variable ENV['APP_FOR']. This can either be "app-1" or "app-2". app-1 validates for username while app-2 validates for email address. Here is my User model spec for app-1:
require 'rails_helper'
RSpec.describe User, type: :model do
include Shared::Categories
before do
ENV['APP_FOR']='app-1'
end
context "given a valid User" do
before { allow_any_instance_of(User).to receive(:older_than_18?).and_return(true) }
it {should validate_presence_of :username}
end
end
And this is the User model spec for app-2
require 'rails_helper'
RSpec.describe User, type: :model do
include Shared::Categories
before do
ENV['APP_FOR']='app-2'
end
context "given a valid User" do
before { allow_any_instance_of(User).to receive(:older_than_18?).and_return(true) }
it {should validate_presence_of :email}
end
end
My problem is that the environment variable isn't being set as I would expect it to be in the before block. Any ideas on how to do this?
EDIT 1
Here is my validation implementation. I used a concern which I extend the user model with:
module TopDogCore::Concerns::UserValidations
extend ActiveSupport::Concern
included do
if ENV['APP_FOR'] == 'app-1'
validates :username,
presence: true,
uniqueness: true
elsif ENV['APP_FOR'] == 'app-2'
validates :email,
presence: true,
uniqueness: true
end
end
end
RSpec loads the subject class before running code in the examples. When you do this:
before do
ENV['APP_FOR'] = # ...
end
it is too late. The class definition has already been executed. You can see this for yourself by simply printing the value of ENV['APP_FOR'] from within the class definition (in your case, the included concern). It is nil, since the environment variable was not set when the class source file was loaded.
Deferring evaluation by using a lambda (as suggested here) ought to work. You might try using your own test instead of the one provided by shoulda_matchers, eg.:
expect(subject.valid?).to be false
expect(subject.errors[:username].blank?).to be false
Try it
module TopDogCore::Concerns::UserValidations
extend ActiveSupport::Concern
included do
validates :username,
presence: true,
uniqueness: true, if: -> { ENV['APP_FOR'] == 'app-1' }
validates :email,
presence: true,
uniqueness: true, if: -> { ENV['APP_FOR'] == 'app-2' }
end
end
Stub your constant like this
before do
stub_const("APP_FOR", 'app-2')
end
Checkout the Stubbing Constants docs
Related
I'm trying to add validations to my mobility-powered application and i'm confused a little.Earlier I've used code like this
I18n.available_locales.each do |locale|
validates :"name_#{locale}", presence: true, uniqueness: {scope: :animal_type}
end
And it worked fine. But in my last project it doesn't work at all. Any ideas how to perform validations? My config is below:
Mobility.configure do
plugins do
backend :container
active_record
reader
writer
backend_reader
query
cache
presence
locale_accessors
end
end
UPD: I've identified my problem - it is because of , uniqueness: {scope: :animal_type}. Is it possible to use mobility with similar type of validations?
When you use uniqueness validator it uses query to the database to ensure that record haven't already taken and you get this query:
Let's assume you have Animal model
SELECT 1 AS one FROM "animals" WHERE "animals"."name_en" = "Cat" AND "animals"."animal_type" = "some_type" LIMIT 1
And of course there is no name_en field in the animals table that is why you have an error
To archive this you have to write your own validator
class CustomValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if translation_exists?(record, attribute)
record.errors.add(attribute, options[:message] || :taken)
end
end
private
def translation_exists?(record, attribute)
attribute, locale = attribute.to_s.split('_')
record.class.joins(:string_translations).exists?(
mobility_string_translations: { locale: locale, key: attribute },
animals: { animal_type: record.animal_type }
)
end
end
And then in your model do next:
I18n.available_locales.each do |locale|
validates :"name_#{locale}", presence: true, custom: true
end
I have a Rails 5 setup where RSpec fails to check validations on model subclass. If I manually build the object in console I am able to see the errors which should prevent the record to be valid.
The base model:
class Article < ApplicationRecord
belongs_to :author, class_name: User
validates :author, presence: { message: "L'utente autore dell'articolo è obbligatorio." }
validates :title, presence: { message: "Il titolo dell'articolo è obbligatorio." }
end
The model which inherits from Article:
class LongArticle < Article
mount_uploader :thumbnail, LongArticleThumbnailUploader
validates :excerpt, presence: { message: "L'estratto dell'articolo è obbligatorio." }
validates :thumbnail, presence: { message: "L'immagine di anteprima dell'articolo è obbligatoria." }
end
The factory for these models (FactoryGirl):
FactoryGirl.define do
factory :article do
association :author, factory: :author
title "Giacomo Puccini: Tosca"
factory :long_article do
type "LongArticle"
excerpt "<p>Teatro alla Scala: immenso Franco Corelli.</p>"
thumbnail { Rack::Test::UploadedFile.new(File.join(Rails.root, 'spec', 'support', 'images', 'unresized-long-article-thumbnail.jpg')) }
end
end
end
This is the RSpec which doesn't work:
require 'rails_helper'
RSpec.describe LongArticle, type: :model do
describe "is valid with mandatory fields" do
it "should be valid with if all mandatory fields are filled" do
article = FactoryGirl.create(:long_article)
expect(article).to be_valid
end
it "should have an excerpt" do
article = FactoryGirl.create(:long_article)
article.excerpt = nil
expect(article).not_to be_valid
end
it "should have the thumbnail" do
article = FactoryGirl.create(:long_article)
article.thumbnail = nil
expect(article).not_to be_valid
end
end
end
The first spec pass, the other two don't.
I tried to test everything in the console, with the same values, and it works, meaning that the record is invalid as it should be.
Is it possible that with RSpec the validations in the subclass won't work?
I'm sorry for the delay, but I think I have figured out what was breaking my tests.
The problems, actually, were two, and not one as I originally thought.
The first test: should have an excerpt
As suggested by juanitofatas, I have added a byebug line after the one where FactoryGirl builds my model. I have noticed that the model instantiated had class Article and not LongArticle.
I came up noticing that FactoryGirl instantiate a model of the base factory when it first met factory :article do. Then, it adds or overrides the attributes defined into inner factories and treating the type attribute as any one other, ignoring that it drives the STI.
The LongArticle factory should have been defined as a completely different model, at the same level as the Article one is.
The second test: should have the thumbnail
This was a bit silly... I have defined a default_url method in the CarrierWave uploader and, in fact, this is the desired behavior. Test was updated.
I'm trying to test the set_random_token method seen below from my model
class Invitation < ActiveRecord::Base
has_and_belongs_to_many :users
validates :name, presence: true, uniqueness: true
validates :number_of_uses, presence: true, numericality: true
validates :token, presence: true, uniqueness: true, format: /\A[0-9A-F]+\z/i
before_create :set_random_token
private
def set_random_token
random_hex = SecureRandom.hex(8)
self.token = random_hex
end
end
Here is the piece in my invitation_spec
it 'verifies that the token is set' do
#invitation = Invitation.new
#invitation.save
expect(#invitation.token).not_to be_empty
end
And here is the error I'm getting
2) Invitation verifies that the token is set
Failure/Error: expect(#invitation.token).not_to be_empty
expected to respond to `empty?`
# ./spec/models/invitation_spec.rb:37:in `block (2 levels) in <top (required)>'
I'm pretty new to Rails, so I apologize if the answer is extremely obvious.
I've seen times where checking the contents of an attribute/column from a local or instance variable of a record didn't yield the same expected result. The workaround I've used in the past is to query for the record with .first and check the column on that.
To ensure you are actually getting the correct record you add a expect(Table.count).to eq 0 at the beginning.
Give this a try
it 'verifies that the token is set' do
expect(Invitation.count).to eq 0
Invitation.new().save(validate: false)
expect(Invitation.count).to eq 1
expect(Invitation.first.token).not_to be_empty
end
From the Rails Documentation:
The following methods trigger validations, and will save the object to the database only if the object is valid:
save
In your test, you are instantiating an empty Invitation but Invitation has a number of validations on it which would make an empty one invalid.
Try this instead:
#invitation = Invitation.create({name: 'something unique', number_of_uses: 5})
expect(#invitation.token).not_to be_empty
#invitation.save
I just ran across Rails concerns and I want to use them for the validations of my models. But I want the validations to be generic, so that the validation is used only if the Class in which I include my concern has the attribute. I thought it would be easy, but I have tried many ways like using column_names, constantize, send and many other but nothing works. What is the right way to do it? The code:
module CommonValidator
extend ActiveSupport::Concern
included do
validates :email, presence: { message: I18n.t(:"validations.commons.email_missing") },
format: { with: /\A[A-Z0-9._%+-]+#[A-Z0-9.-]+\.[A-Z]{2,4}\z/i,
message: I18n.t(:"validations.commons.email_wrong_format"),
allow_blank: true } if self.column_names.include? :email
end
end
class Restaurant < ActiveRecord::Base
include CommonValidator
.
.
.
end
Restaurant of course has an email attribute. Is it possible to check the existence of an attribute in the class in which in include my concern? I want include my CommonValidations into many models which will not have email attribute. I'm using rails 4.
You can use respond_to? on the current instance as follows:
validates :email, presence: { message: I18n.t(:"validations.commons.email_missing") },
format: { with: /\A[A-Z0-9._%+-]+#[A-Z0-9.-]+\.[A-Z]{2,4}\z/i,
message: I18n.t(:"validations.commons.email_wrong_format"),
allow_blank: true },
if: lambda { |o| o.respond_to?(:email) }
Another option as suggested by #coreyward is to define a class extending EachValidator. For example, for email validation:
# app/validators/email_validator.rb
class EmailValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless value =~ /\A[A-Z0-9._%+-]+#[A-Z0-9.-]+\.[A-Z]{2,4}\z/i
record.errors[attribute] << (options[:message] || I18n.t(:"validations.commons.email_wrong_format"))
end
end
end
Then you could update the validation call as:
validates :email,
presence: { message: I18n.t(:"validations.commons.email_missing") },
email: true,
allow_blank: true
I was looking for something similar, but with custom validations.
I ended up with something that I think could be shared, including generic tests.
First, set up the concern app/models/concern/my_concern.rb.
Please note that we don't define the validate_my_field into a ClassMethods module.
module MyConcern
extend ActiveSupport::Concern
included do
validate :my_field, :validate_my_field
end
private
def validate_my_field
...
end
end
Include concern into your model app/models/my_model.rb
class MyModel < ActiveRecord::Base
include MyConcern
end
Load concerns shared examples in spec/support/rails_helper:
…
Dir[Rails.root.join('spec/concerns/**/*.rb')].each { |f| require f }
…
Create concern shared examples spec/concerns/models/my_field_concern_spec.rb:
RSpec.shared_examples_for 'my_field_concern' do
let(:model) { described_class } # the class that includes the concern
it 'has a valid my_field' do
instance = create(model.to_s.underscore.to_sym, my_field: …)
expect(instance).not_to be_valid
…
end
end
Then finally call shared examples into your model spec spec/models/my_model_spec.rb:
require 'rails_helper'
RSpec.describe MyModel do
include_examples 'my_field_concern'
it_behaves_like 'my_field_concern'
end
I hope this could help.
I have been starting to learn testing in my Rails app and am using rSpec and Shoulda.
I have the following test which works:
it { should respond_to(:park_name) }
However, what I don't understand is, what is this being run on? Is this being run on the Model itself or an instance of the model and if it's an instance of the Model then is it automatically using my Factory Girl factory?
Any simple explanations on what is actually occurring here?
UPDATE:
Ok, So I have this:
describe 'validations' do
subject { FactoryGirl.build(:coaster) }
it { should validate_presence_of(:name) }
it { should validate_presence_of(:speed) }
it { should validate_presence_of(:height) }
end
But the tests are failing. Any ideas?
Coaster.rb:
class Coaster < ActiveRecord::Base
extend FriendlyId
friendly_id :slug, use: :slugged
belongs_to :park
belongs_to :manufacturer
attr_accessible :name,
:height,
:speed,
:length,
:inversions,
:material,
:lat,
:lng,
:park_id,
:notes,
:manufacturer_id,
:style,
:covering,
:ride_style,
:model,
:layout,
:dates_ridden,
:times_ridden,
:order,
:on_ride_photo
scope :by_name_asc, lambda {
order("name ASC")
}
scope :made_from, lambda { |material|
where("material = ?", material)
}
scope :wooden, lambda {
made_from "wood"
}
scope :steel, lambda {
made_from "steel"
}
delegate :name, :location_1, :location_2, :location_3, :location_4,
to: :park,
allow_nil: true,
prefix: true
delegate :name, :url,
to: :manufacturer,
prefix: true
validates :name,
:presence => true
validates :height,
allow_nil: true,
numericality: {greater_than: 0}
validates :speed,
allow_nil: true,
numericality: {greater_than: 0}
validates :length,
allow_nil: true,
numericality: {greater_than: 0}
Test Results:
1) Coaster validations should require speed to be set
Failure/Error: it { should validate_presence_of(:speed) }
Expected errors to include "can't be blank" when speed is set to nil, got no errors
# ./spec/models/coaster_spec.rb:75:in `block (3 levels) in '
2) Coaster validations should require height to be set
Failure/Error: it { should validate_presence_of(:height) }
Expected errors to include "can't be blank" when height is set to nil, got no errors
# ./spec/models/coaster_spec.rb:76:in `block (3 levels) in '
SIMILAR QUESTION:
I have this test:
describe 'methods' do
subject { FactoryGirl.build(:coaster) }
it "should return a formatted string of coaster name at park name" do
name_and_park.should eq('Nemesis at Alton Towers')
end
end
Coaster.rb:
def name_and_park
[name, park.name].join (' at ')
end
Error when running the test:
2) Coaster methods should return a formatted string of coaster name at
park name
Failure/Error: name_and_park.should eq('Nemesis at Alton Towers')
NameError:
undefined local variable or method name_and_park' for #<RSpec::Core::ExampleGroup::Nested_1::Nested_6:0x007f84f4161798>
# ./spec/models/coaster_spec.rb:111:inblock (3 levels) in '
It says name_and_park cannot be called but surely that method should be being called on the instance of Coaster that is being made in the subject line? No?
It's being run on "subject", which is either explicitly defined through the subject method or implicitly defined by passing in a class as the argument to describe, in which case an instance of that class is instantiated and made the subject of the test. See https://www.relishapp.com/rspec/rspec-core/v/2-0/docs/subject/explicit-subject and https://www.relishapp.com/rspec/rspec-core/v/2-0/docs/subject/implicit-subject
As for the answer to the Update question, in your model validations for :speed and :length, you have allow_nil: true, which is why those two tests are failing. Part of the definition of validates_presence_of is that nil is not a settable value.
As for your latest question, I think you may be confused about the use of implicit subjects. If should is used by itself, it will indeed default to whatever the subject is, but if you include a subject yourself, as you have in this case with name_and_park, it won't treat that as a method of the default subject, it must have a definition within the current namespace. In your case, you would need to say subject.name_and_park.should ....
On a related aside, StackOverflow is best used when you ask a specific question or related set of questions and get an answer. For a variety of reasons, it's not intended for ongoing debugging sessions. One of those reasons is that it becomes tedious tracking substantial, sequential updates of the original question and answer.