Here is the relevant section from my models:
belongs_to :cart
belongs_to :product
validate :quantity, :more_than_stock, :message => "more than in stock is reserved."
def more_than_stock
errors.add(:quantity, "should be less than in stock") if self.quantity > self.product.stock
end
I keep erroring out on this line: errors.add(:quantity, "should be less than in stock") if self.quantity > self.product.stock with regards to the .stock method.
The error I keep getting is: 1) Error:
test_product_id_must_be_a_number(CartRowTest):
NoMethodError: undefined method 'stock' for nil:NilClass in my tests.
It seems to me that my test suite doesn't know about the .stock method on product.
However, here is my product factory:
factory :product do
name 'Cholecap - 100mg'
limit 3
stock 10
end
and my cart_row factory:
factory :cart_row do
product
cart
quantity 3
end
Here is the relevant portion of my unit test that throws the error:
def setup
#cart_row = FactoryGirl.create(:cart_row)
end
test "product_id must not be blank" do
#cart_row.product_id = " "
assert !#cart_row.valid?
end
test "product_id must be a number" do
#cart_row.product_id = '234'
assert !#cart_row.valid?
end
What do I need to do to let the test suite know about the .stock method?
Because you set product_id to invalid value you can't make test suite know about the #stock method. If you realy want make these test pass try this code:
belongs_to :cart
belongs_to :product
validates_associated :product
validate :quantity, :more_than_stock, message: "more than in stock is reserved." , if: "product.respond_to? :stock"
def more_than_stock
errors.add(:quantity, "should be less than in stock") if quantity > product.stock
end
Related
I work on a booking application, where each Home can have several Phone.
I would like to limit the number of Phone by Home to 3, and display a nice error in the create phone form.
How can I achieve that, in a rails way ?
code
class Phone < ApplicationRecord
belongs_to :user
validates :number, phone: true
# validates_associated :homes_phones
has_many :homes_phones, dependent: :destroy
has_many :homes, through: :homes_phones
end
class User < ApplicationRecord
has_many :phones, dependent: :destroy
end
class HomesPhone < ApplicationRecord
belongs_to :home
belongs_to :phone
validate :check_phones_limit
def check_phones_limit
errors.add(:base, "too_many_phones") if home.phones.size >= 3
end
end
specs
it 'should limit phones to 3' do
user = create(:user)
home = create(:home, :active, manager: user)
expect(home.phones.create(user: user, number: "+33611223344")).to be_valid
expect(home.phones.create(user: user, number: "+33611223345")).to be_valid
expect(home.phones.create(user: user, number: "+33611223346")).to be_valid
# unexpectedly raises a ActiveRecord::RecordInvalid
expect(home.phones.create(user: user, number: "+33611223347")).to be_invalid
end
Side notes
My understanding of the flow is:
a transaction opens
phone attributes are validated (and valid)
phone is created, a primary key is available
homes_phone is saved! and an error is raised because the validation fails
the all transaction is rolled back, and the error bubble up
I have tried:
has_many before_add in Home which also raise an error;
validating these rules in Phone does not make sense to me, as this rule is a Home concern
You can just validate it in the controller, count the phones and render a flash error, before you actually try and save the records.
Doing this in callbacks is difficult and not foolproof.
I've added some quick tests to cover different ways phones can be created:
# spec/models/homes_phone_spec.rb
require "rails_helper"
RSpec.describe HomesPhone, type: :model do
it "saves 3 new phones" do
home = Home.create(phones: 3.times.map { Phone.new })
expect(home.phones.count).to eq 3
end
it "doesn't save 4 new phones" do
home = Home.create(phones: 4.times.map { Phone.new })
expect(home.phones.count).to eq 0
expect(home.phones.size).to eq 4
end
it "can create up to 3 three phones through association" do
home = Home.create!
expect do
5.times { home.phones.create }
end.to change(home.phones, :count).by(3)
end
it "doesn't add 4th phone to existing record" do
Home.create(phones: 3.times.map { Phone.new })
home = Home.last
# NOTE: every time you call `valid?` or `validate`, it runs validations
# again and all previous errors are cleared.
# expect(home.phones.create).to be_invalid
expect(home.phones.create).to be_new_record
# or
# expect(home.phones.create).not_to be_persisted
# expect(home.phones.create.errors.any?).to be true
end
it "adds phone limit validation error to Phone" do
home = Home.create(phones: 3.times.map { Phone.new })
phone = home.phones.create
expect(phone.errors[:base]).to eq ["too many phones"]
end
end
# app/models/phone.rb
class Phone < ApplicationRecord
has_many :homes_phones, dependent: :destroy
has_many :homes, through: :homes_phones
end
# app/models/home.rb
class Home < ApplicationRecord
has_many :homes_phones, dependent: :destroy
has_many :phones, through: :homes_phones
end
# app/models/homes_phone.rb
class HomesPhone < ApplicationRecord
belongs_to :home
# NOTE: setting `inverse_of` option will stop raising errors and
# just return `phone` with errors that we'll add below
belongs_to :phone, inverse_of: :homes_phones
# because of the way through association is saved with `save!` call
# it raises validation errors. `inverse_of` allows the through association
# to be properly built and it skips `save!` call:
# https://github.com/rails/rails/blob/v7.0.4.2/activerecord/lib/active_record/associations/has_many_through_association.rb#L79-L80
validate do
# NOTE: if you use `homes_phones` association, the `size` method returns
# current count, instead of a lagging `phones.size` count.
if home.homes_phones.size > 3
errors.add(:base, "too many phones")
phone.errors.merge!(errors)
end
end
end
$ rspec spec/models/homes_phone_spec.rb
HomesPhone
saves 3 new phones
doesn't save 4 new phones
can create up to 3 three phones through association
doesn't add 4th phone to existing record
adds phone limit validation to Phone
Finished in 0.10967 seconds (files took 2.35 seconds to load)
5 examples, 0 failures
But it doesn't cover everything, like this:
it "can append up to 3 three phones" do
home = Home.create!
expect do
5.times { home.phones << Phone.new }
end.to change(home.phones, :count).by(3)
end
$ rspec spec/models/homes_phone_spec.rb:38
Run options: include {:locations=>{"./spec/models/homes_phone_spec.rb"=>[38]}}
HomesPhone
can append up to 3 three phones (FAILED - 1)
and I thought I fixed everything. You can try this instead:
after_validation do
if home.homes_phones.size > 3
errors.add(:base, "too many phones")
phone.errors.merge! errors
raise ActiveRecord::Rollback
end
end
$ rspec spec/models/homes_phone_spec.rb -f p
......
Finished in 0.12411 seconds (files took 2.25 seconds to load)
6 examples, 0 failures
This is the rails way of doing this, in your Home class
validates :phones, length: { maximum: 3, message: "too many phones" }
You could think validation is something like this: it target to the Model we want to create directly.
In your case, you want to validate when create phones, so you have to put the validation rule into the Phone model.
This way the validation will take effect, and you could get the error message correctly.
I get the following error when running rake test:
Error:
ShareTest#test_valid_setup:
ActiveRecord::RecordInvalid: Validation failed: Email has already been taken
test/models/share_test.rb:7:in `setup'
Why? I'm really not sure what this means, because I get the error even when I override the email parameter in my setup. My other tests pass just fine. My model associations are written so that:
-User has_many Items, Trades, and Shares
-Trade belongs_to User and Share
-Share belongs_to User and Item
-Item belongs_to User and has_many Trades and Shares
The related factories are:
FactoryBot.define do
factory :user, aliases: [:owner, :trade_requester, :trade_recipient, :share_owner] do
email Faker::Internet.email
firstname Faker::Name.first_name
lastname Faker::Name.last_name
username Faker::Pokemon.name
website Faker::Internet.domain_name
end
factory :item do
owner
image { Rack::Test::UploadedFile.new(Rails.root.join('test', 'lilac.jpg'), 'image/jpeg') }
title Faker::Book.title
artist Faker::Book.author
medium Faker::Book.genre
year Faker::Number.number(4)
price Faker::Number.between(1, 5)
length Faker::Number.between(1, 2)
height Faker::Number.between(1, 2)
width Faker::Number.between(1, 2)
agreement true
end
factory :share, aliases: [:wanted_share, :collateral_share] do
user
item
amount { Item.price }
active true
end
end
And then the actual test where I'm getting the duplicate email error is:
require 'test_helper'
require 'support/database_cleaner'
class ShareTest < Minitest::Test
def setup
DatabaseCleaner.start
#share = FactoryBot.create(:share)
end
def test_valid_setup
assert_kind_of Integer, share.amount
end
def teardown
DatabaseCleaner.clean
end
end
I have a simple situation setup in order to learn testing with FactoryGirl. A Bank has many transactions. Each time a transaction is created, it should subtract the transaction amount from the bank's total.
Here is the code:
# Bank.rb - Has many transactions
class Bank < ActiveRecord::Base
has_many :transactions
end
# Transaction.rb - Belongs to a bank and should decrement the bank's total when created.
class Transaction < ActiveRecord::Base
belongs_to :bank
after_create :decrement_bank_amount
def decrement_bank_amount
bank.decrement!(:amount, amount) if bank
end
end
# factories.rb - Create default factories for testing. This is FactoryGirl 4 syntax
FactoryGirl.define do
factory :bank do
sequence(:name) { |n| 'Bank ' + n.to_s }
end
factory :transaction do
sequence(:title) { |n| 'Payment ' + n.to_s }
bank
end
end
# Transaction_spec.rb - Creates a bank and a transaction.
require 'spec_helper'
describe Transaction do
describe ".create" do
context "when a bank is set" do
it "decreases the bank's amount" do
bank = FactoryGirl.create(:bank, :amount => 1000) do |b|
b.transactions.create(:amount => 250)
end
bank.amount.to_i.should eq 750
end
end
end
end
The test keeps failing and the bank amount keeps returning 1000 instead of the expected 750. I'm stumped!
This test is failing because bank is fetched from the database and stored. The after_create callback modifies the record in the database, but the object in bank doesn't see that, and so isn't updated.
You're going to need to call reload on that object before checking the amount:
bank.reload
bank.amount.to_i.should == 750
Consider the following:
ScheduledSession ------> Applicant <------ ApplicantSignup
Points to note:
A ScheduledSession will exist in the system at all times; think of this as a class or course.
The intent here is to validate the ApplicantSignup model against an attribute on ScheduledSession during signups_controller#create
Associations
class ScheduledSession < ActiveRecord::Base
has_many :applicants, :dependent => :destroy
has_many :applicant_signups, :through => :applicants
#...
end
class ApplicantSignup < ActiveRecord::Base
has_many :applicants, :dependent => :destroy
has_many :scheduled_sessions, :through => :applicants
#...
end
class Applicant < ActiveRecord::Base
belongs_to :scheduled_session
belongs_to :applicant_signup
# TODO: enforce validations for presence
# and uniqueness constraints etc.
#...
end
SignupsController
Resources are RESTful, i.e. the #create action will have a path that's similar to /scheduled_sessions/:id/signups/new
def new
#session = ScheduledSession.find(params[:scheduled_session_id])
#signup = #session.signups.new
end
def create
#session = ScheduledSession.find(params[:scheduled_session_id])
#session.duration = (#session.end.to_time - #session.start.to_time).to_i
#signup = ApplicantSignup.new(params[:signup].merge(:sessions => [#session]))
if #signup.save
# ...
else
render :new
end
end
You'll notice I'm setting a virtual attribute above #session.duration to prevent Session from being considered invalid. The real 'magic' if you will happens in #signup = ApplicantSignup.new(params[:signup].merge(:sessions => [#session])) which now means that in the model I can select from self.scheduled_sessions and access the ScheduledSession this ApplicantSignup is being built against, even though at this very point in time, there is no record present in the join table.
Model validations for example look like
def ensure_session_is_upcoming
errors[:base] << "Cannot signup for an expired session" unless self.scheduled_sessions.select { |r| r.upcoming? }.size > 0
end
def ensure_published_session
errors[:base] << "Cannot signup for an unpublished session" if self.scheduled_sessions.any? { |r| r.published == false }
end
def validate_allowed_age
# raise StandardError, self.scheduled_sessions.inspect
if self.scheduled_sessions.select { |r| r.allowed_age == "adults" }.size > 0
errors.add(:dob_year) unless (dob_year.to_i >= Time.now.strftime('%Y').to_i-85 && dob_year.to_i <= Time.now.strftime('%Y').to_i-18)
# elsif ... == "children"
end
end
The above works quite well in development and the validations work as expected — but how does one test with with Factory Girl? I want unit tests to guarantee the business logic I've implemented after all — sure, this is after the fact but is still one way of going about TDD.
You'll notice I've got a commented out raise StandardError, self.scheduled_sessions.inspect in the last validation above — this returns [] for self.scheduled_sessions which indicates that my Factory setup is just not right.
One of Many Attempts =)
it "should be able to signup to a session" do
scheduled_session = Factory.build(:scheduled_session)
applicant_signup = Factory.build(:applicant_signup)
applicant = Factory.create(:applicant, :scheduled_session => scheduled_session, :applicant_signup => applicant_signup)
applicant_signup.should be_valid
end
it "should be able to signup to a session for adults if between 18 and 85 years" do
scheduled_session = Factory.build(:scheduled_session)
applicant_signup = Factory.build(:applicant_signup)
applicant_signup.dob_year = 1983 # 28-years old
applicant = Factory.create(:applicant, :scheduled_session => scheduled_session, :applicant_signup => applicant_signup)
applicant_signup.should have(0).error_on(:dob_year)
end
The first one passes, but I honestly do not believe it's properly validating the applicant_signup model; the fact that self.scheduled_sessions is returning [] simply means that the above just isn't right.
It's quite possible that I'm trying to test something outside the scope of Factory Girl, or is there a far better approach to tackling this? Appreciate all comments, advice and constructive criticism!
Updates:
Not sure what this is called but this is the approach taken at least with regards to how it's implemented at the controller level
I need to consider ignoring Factory Girl for the association aspect at least and attempt to return the scheduled_session by mocking scheduled_sessions on the applicant_signup model.
Factories
FactoryGirl.define do
factory :signup do
title "Mr."
first_name "Franklin"
middle_name "Delano"
last_name "Roosevelt"
sequence(:civil_id) {"#{'%012d' % Random.new.rand((10 ** 11)...(10 ** 12))}"}
sequence(:email) {|n| "person#{n}##{(1..100).to_a.sample}example.com" }
gender "male"
dob_year "1980"
sequence(:phone_number) { |n| "#{'%08d' % Random.new.rand((10 ** 7)...(10 ** 8))}" }
address_line1 "some road"
address_line2 "near a pile of sand"
occupation "code ninja"
work_place "Dharma Initiative"
end
factory :session do
title "Example title"
start DateTime.civil_from_format(:local,2011,12,27,16,0,0)
duration 90
language "Arabic"
slides_language "Arabic & English"
venue "Main Room"
audience "Diabetic Adults"
allowed_age "adults"
allowed_gender "both"
capacity 15
published true
after_build do |session|
# signups will be assigned manually on a per test basis
# session.signups << FactoryGirl.build(:signup, :session => session)
end
end
factory :applicant do
association :session
association :signup
end
#...
end
My earlier assumption was correct, with on small change:
I need to consider ignoring Factory Girl for the association aspect at
least and attempt to return the scheduled_session by stubbing
scheduled_sessions on the applicant_signup model.
making my tests quite simply:
it "should be able to applicant_signup to a scheduled_session" do
scheduled_session = Factory(:scheduled_session)
applicant_signup = Factory.build(:applicant_signup)
applicant_signup.stub!(:scheduled_sessions).and_return{[scheduled_session]}
applicant_signup.should be_valid
end
it "should be able to applicant_signup to a scheduled_session for adults if between 18 and 85 years" do
scheduled_session = Factory(:scheduled_session)
applicant_signup = Factory.build(:applicant_signup)
applicant_signup.dob_year = 1983 # 28-years old
applicant_signup.stub!(:scheduled_sessions).and_return{[scheduled_session]}
applicant_signup.should have(0).error_on(:dob_year)
applicant_signup.should be_valid
end
and this test in particular required a similar approach:
it "should not be able to applicant_signup if the scheduled_session capacity has been met" do
scheduled_session = Factory.build(:scheduled_session, :capacity => 3)
scheduled_session.stub_chain(:applicant_signups, :count).and_return(3)
applicant_signup = Factory.build(:applicant_signup)
applicant_signup.stub!(:scheduled_sessions).and_return{[scheduled_session]}
applicant_signup.should_not be_valid
end
...and success — ignore the testing duration as spork causes false reporting of this.
Finished in 2253.64 seconds
32 examples, 0 failures, 3 pending
Done.
As another approach you could use Rspecs stub_model.
Also, if you test ApplicantSignup, you should init it and not test the creation of the Applicant. Eg:
applicant_signup = Factory.build(:applicant_signup);
applicant_signup.should_receive(:scheduled_sessions)
.and_return{[scheduled_session]};
So there will be less DB access and you will test ApplicantSignup, not Applicant.
Hi i'm using a has_and_belongs_to_many in a model.
I want set the valitor of presence for kinds.
and set the max number of kinds per core to 3
class Core < ActiveRecord::Base
has_and_belongs_to_many :kinds, :foreign_key => 'core_id', :association_foreign_key => 'kind_id'
end
how can i do?
thanks
validate :require_at_least_one_kind
validate :limit_to_three_kinds
private
def require_at_least_one_kind
if kinds.count == 0
errors.add_to_base "Please select at least one kind"
end
end
def limit_to_three_kinds
if kinds.count > 3
errors.add_to_base "No more than 3 kinds, please"
end
end
You could try something like this (tested on Rails 2.3.4):
class Core < ActiveRecord::Base
has_and_belongs_to_many :kinds, :foreign_key => 'core_id', :association_foreign_key => 'kind_id'
validate :maximum_three_kinds
validate :minimum_one_kind
def minimum_one_kind
errors.add(:kinds, "must total at least one") if (kinds.length < 1)
end
def maximum_three_kinds
errors.add(:kinds, "must not total more than three") if (kinds.length > 3)
end
end
... which works in the following way:
require 'test_helper'
class CoreTest < ActiveSupport::TestCase
test "a Core may have kinds" do
core = Core.new
3.times { core.kinds << Kind.new }
assert(core.save)
end
test "a Core may have no more than 3 kinds" do
core = Core.new
4.times { core.kinds << Kind.new }
core.save
assert_equal(1, core.errors.length)
assert_not_nil(core.errors['kinds'])
end
test "a Core must have at least one kind" do
core = Core.new
core.save
assert_equal(1, core.errors.length)
assert_not_nil(core.errors['kinds'])
end
end
Obviously the above isn't particularly DRY or production-ready, but you get the idea.