Rails nested model creation not respecting validation constraints - ruby-on-rails

I have a Customer model that has_many polymorphic Address objects, like so:
Customer Model:
class Customer < ActiveRecord::Base
has_many :mailing_addresses, :as => :addressable, :class_name => 'Address', dependent => :destroy
accepts_nested_attributes_for :mailing_addresses
validates :mailing_addresses, :presence => true
validates_associated :mailing_addresses
end
Address Model:
class Address < ActiveRecord::Base
belongs_to :addressable, :polymorphic => true
validate :validate_quota
private
def validate_quota
case addressable_type
when "Customer"
customer = Customer.find(addressable_id)
if customer.mailing_addresses.size >= 3
puts "Adding too many records"
errors.add(:addressable, "Too many records")
end
end
end
I am also using RSpec to test that the quota constraint is respected. So this spec passes, for example:
it "observes quota limit" do
customer = FactoryGirl.create(:customer, :number_of_mailing_addresses => 3)
expect {
address = FactoryGirl.create(:mailing_address, :addressable => customer)
}.to raise_error
customer.mailing_addresses.count.should eq(3)
end
Which is good. However, this fails horribly:
it "fails add if already 3 addresses" do
customer = FactoryGirl.create(:customer, :number_of_mailing_addresses => 3)
expect {
customer.mailing_addresses.create( FactoryGirl.attributes_for(:mailing_address).except(:addressable) )
}.to raise_error
end
I can even see in the spec output that the Address validation is failing, but for some reason it doesn't raise an error in "customer.mailing_addresses.create()", nor does that failed validation prevent the 4th Address model from saving to the database.
What am I missing?

Ah, I finally figured it out! I needed to change create to create! in my last spec in order to make the failing validation actually throw an error.

Related

Rspec: How to create mock association

I have following class:
class Company < ActiveRecord::Base
validates :name, :presence => true
has_many :employees, :dependent => :destroy
end
class Employee < ActiveRecord::Base
validates :first_name, :presence => true
validates :last_name, :presence => true
validates :company, :presence => true
belongs_to :company
end
I am writing test for Employee class, so I am trying to create double for Company which will be used by Employee.
Below is the snippet for my Rspec
let(:company) { double(Company) }
let(:employee) { Employee.new(:first_name => 'Tom', :last_name => 'Smith', :company => company) }
context 'valid Employee' do
it 'will pass validation' do
expect(employee).to be_valid
end
it 'will have no error message' do
expect(employee.errors.count).to eq(0)
end
it 'will save employee to database' do
expect{employee.save}.to change{Employee.count}.from(0).to(1)
end
end
I am getting following error message for all of my 3 tests
ActiveRecord::AssociationTypeMismatch:
Company(#70364315335080) expected, got RSpec::Mocks::Double(#70364252187580)
I think the way I am trying to create double is wrong. Can you please guide me how to create a double of Company which can be used by Employee as their association.
I am not using FactoryGirl.
Thanks a lot.
There is not really a great way to do this, and I'm not sure you need to anyway.
Your first two tests are essentially testing the same thing (since if the employee is valid, employee.errors.count will be 0, and vice versa), while your third test is testing the framework/ActiveRecord, and not any of your code.
As other answers have mentioned, Rails wants the same classes when validating in that way, so at some point you'll have to persist the company. However, you can do that in just one test and get the speed you want in all the others. Something like this:
let(:company) { Company.new }
let(:employee) { Employee.new(:first_name => 'Tom', :last_name => 'Smith', :company => company) }
context 'valid Employee' do
it 'has valid first name' do
employee.valid?
expect(employee.errors.keys).not_to include :first_name
end
it 'has valid last name' do
employee.valid?
expect(employee.errors.keys).not_to include :last_name
end
it 'has valid company' do
company.save!
employee.valid?
expect(employee.errors.keys).not_to include :company
end
end
And if you really want to keep your third test, you can either include company.save! in your it block, or disable validation (though, again, what are you even testing at that point?):
it 'will save employee to database' do
expect{employee.save!(validate: false)}.to change{Employee.count}.from(0).to(1)
end
There is already similar question on SO (Rspec Mocking: ActiveRecord::AssociationTypeMismatch). I think you can't get away from using real AR objects 'cause it seems that Rails checks exact class of association object and double is an instance of some absolutely different class. Maybe you could stub some inner Rails' methods to skip that check but I think it's an overhead.

Running custom validations with gem specific methods

I setup a custom validation that checks if a user has voted for an album before submitting a review. The validation works fine on the client side but when it comes to running my Rspec tests I seem to run into some problems.
The validation makes use of the Acts As Votable gem's voted_for? method. Unfortunately this is where things go bad. For my non-custom validations (that do work regularly btw) I get an error like this:
3) Review validations should ensure body has a length of at least 40
Failure/Error: it { should validate_length_of(:body).is_at_least(40) }
NoMethodError:
undefined method `voted_for' for nil:NilClass
What do I need to do in order for this method to be recognized?
Review Model
class Review < ActiveRecord::Base
belongs_to :album
belongs_to :owner, class_name: "User", foreign_key: :user_id
validates :body, presence: true, length: { minimum: 40 }
def album_vote
if !owner.voted_for?(album)
errors.add(:review, "requires an album vote before submitting")
end
end
end
Review Factory
FactoryGirl.define do
factory :review do
body { Faker::Lorem.paragraph(2) }
album
association :owner, factory: :user
end
end
You need to ensure that there actually is an owner. The error is simply because you are calling on nil and does not really have anything to do with ActsAsVotable.
class Review < ActiveRecord::Base
belongs_to :album
belongs_to :owner, class_name: "User", foreign_key: :user_id
validates :body, presence: true, length: { minimum: 40 }
validates :owner, presence: true
def album_vote
# consider raising an error if there is no owner.
if owner && !owner.voted_for(album)
errors.add(:review, "requires an album vote before submitting")
end
end
end
And then change your factory definition to create the owner:
FactoryGirl.define do
factory :user, aliases: [:owner] do
# ...
end
end
FactoryGirl.define do
factory :review do
body { Faker::Lorem.paragraph(2) }
album
owner
end
end

validation error through FactoryGirl

I have problems on validating my data through FactoryGirl.
For team.rb, the custom validations are has_only_one_leader and belongs_to_same_department
class Team < ActiveRecord::Base
attr_accessible :name, :department, :active
has_many :memberships
has_many :users, through: :memberships
accepts_nested_attributes_for :memberships, :users
validates :department, inclusion: [nil, "Architectural", "Interior Design"]
validates_uniqueness_of :name, scope: :department, conditions: -> { where(active: true) }
validate :has_only_one_leader
validate :belongs_to_same_department
def has_only_one_leader
unless self.users.where!(designation: "Team Leader").size == 1
errors.add(:team, "needs to have exactly one leader")
end
end
def belongs_to_same_department
unless self.users.where!.not(department: self.department).size == 0
errors.add(:users, "should belong to the same department")
end
end
I'll also include membership.rb and user.rb (associations only) just for reference.
class Membership < ActiveRecord::Base
belongs_to :team
belongs_to :user
end
class User < ActiveRecord::Base
has_many :memberships
has_many :teams, through: :memberships
end
Here's my factory for team.rb
FactoryGirl.define do
factory :team do
sequence(:name) {|n| "Team #{n}" }
department "Architectural"
before(:create) do |team|
team.users << FactoryGirl.create(:user, designation: "Team Leader",
department: "Architectural")
team.users << FactoryGirl.create_list(:user, 5,
designation: "CAD Operator", department: "Architectural")
end
end
end
It seems that after I do FactoryGirl.create(:team) in the rails console, it seems that I got the error messages from my validations.
Two things I've noticed when I manually build a team, specifically adding members to the team with 1 leader and 5 members belonging to the same department:
team.users.where!(designation: "Team Leader").size returns 6 although there's only one leader, same goes for changing the designation to CAD Operator, returns 6 although there are only five non leaders in the team.
same goes for checking if the members are in the same department, team.users.where!.not(department: team.department).size returns 6, although all of them belong to the same department.
Are there any modifications to make in my model or in my factory?
Here are the things that I've discovered before getting the answer.
team.users.where(designation: "Team Leader") returns a User::ActiveRelation_AssociationRelation object
team.users returns a User::ActiveRecord_Associations_CollectionProxy object
CollectionProxy has no method where since the this method (in my opinion) is a query to the database (with exception if the team is already saved in the database, you can use where, but in this case, it's only for building)
Therefore, I used the select method accompanied with the count, to return the correct values like so. I'll use my example from the question to illustrate the answer.
team.users.select(:designation) {|user| user.designation == "Team Leader"}.count returns 1
team.users.select(:department) {|user| user.department != team.department}.count returns 0

Deleting children does work in real app, but not in test

I can't for the life of me figure out what's going wrong here. I have a Client model and an Invoice model.
Client.rb
has_many :invoices, dependent: :destroy
Invoices.rb
belongs_to :client
I have the following client spec:
it "destroys its children upon destruction" do
i = FactoryGirl.create(:invoice) # As per the factory, the :client is the parent of :invoice and is automatically created with the :invoice factory
lambda {
i.client.destroy
}.should change(Invoice.all, :count).by(-1)
end
And here are my factories:
Client factory
FactoryGirl.define do
factory :client do
city "MyString"
end
end
Invoice factory
FactoryGirl.define do
factory :invoice do
association :client
gross_amount 3.14
end
end
If I do i = FactoryGirl.create(:invoice) and afterwards i.client.destroy manually in the console, the invoice is in fact destroyed. But for some reason, the test fails, giving me "count should have been changed by -1, but was changed by 0".
What am I doing wrong?
The return value of Invoice.all is an array and so database operations won't affect it. The array doesn't change when you destroy records, and should change(receiver, message) is going to just send message to the same receiver. Try either:
lambda {
i.client.destroy
}.should change(Invoice, :count).by(-1)
or
lambda {
i.client.destroy
}.should change{Invoice.all.count}.by(-1)

Rails 3.1.0 conditional validations on nested attributes

I have a structure where an Item may belong to a Claim, and if it does, I want another of its fields to be required as well. These are the relevant code snippets:
class Claim
has_many :items
accepts_nested_attributes_for :items
validates_associated :items
end
class Item
belongs_to :claim
validates :amount_paid, :presence => {:if => :claim}
end
And this works in almost every case. When I edit an existing Claim and try to enter blanks in the amount_paid field, I get the errors I want. And the Claim should exist when it hits this validation, because a previous iteration, which also worked, had the equivalent of
validates :claim_id, :presence => {:unless => :new_claim?}
...
def new_claim?
claim.new_record? # would have thrown an error if claim was nil
end
But when I create a new Claim with blank amount_paid fields on its Items, the validations pass, which they shouldn't.
To no avail, I have also tried
validates :amount_paid, :presence => {:if => :claim_exists?}
...
def claim_exists?
!!claim
end
Any other ideas?
What I've done is possibly a bit of a hack, but it seems to work:
class Item
...
validates :amount_paid, :presence => {:if => :claimed?}
...
def claimed?
!!claim || caller.any? { |m| m =~ /claims_controller/ }
end
end
So if the claim exists, or if this is being called from ClaimsController at any point in the stack trace, the validation will run.
I'd still welcome input from anyone who has a better idea.
I believe the problem can be fixed by adding an :inverse_of option to the associations:
class Claim
has_many :items, :inverse_of => :claim
end
class Item
belongs_to :claim, :inverse_of => :items
end
(It's been a while since I came across this, though, so if you're having the same problem as I was, do a bit of experimentation.)

Resources