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
Related
I an invoice model with many invoice line items. On the invoice model, there is a before save callback that totals the invoice line items and sets the total field on the invoice:
# invoice.rb model
before_save :set_total
#...
private
def set_total
self.total = invoice_line_items.sum("quantity * price")
end
Here is the invoice factory:
FactoryBot.define do
factory :invoice do
due_date {30.days.from_now}
notes {"Some notes here, chap."}
paid {false}
paid_date {nil}
factory :invoice_with_line_items do
invoice_line_items { build_list :invoice_line_item, 2 }
end
service_order
end
end
And some test code:
RSpec.describe Invoice, type: :model do
let(:invoice) {
create(:invoice_with_line_items)
}
describe "#total" do
it "calculates total of all line items" do
binding.pry
expect(invoice.total).to eq (50.99*4).to_d
end
end
However, in my rspec tests that use factory bot, the total is always set to 0 almost as if the callback is not firing at all or is firing before the associations are created.
How can I get the before save callback to work so that it sees the association?
I found out that the problem didn't lie in factory bot, but rather with my logic in the before save callback. SUM depends on the database, so new records aren't seen. I updated the code to the following and it works fine:
# invoice.rb model before_save callback
def set_total
ili_total = 0
invoice_line_items.each do |ili|
ili_total += ili.quantity * ili.price
end
self.total = ili_total
end
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
There is a Company class that has_many QuarterValue, and I have a RSpec test for it.
let(:company) { Company.create }
describe 'company has many quarter values' do
before do
10.times { create(:quarter_value, company: company) }
end
it 'has 10 quarter values' do
expect(company.quarter_values.count).to eq(10)
end
end
The test passes. My question is when I put binding.pry just above the expect matcher I can't access company.quarter_values, that returns empty array [].
How can I access has_many models object in RSpec test by using binding.pry?
spec/factories.rb
FactoryGirl.define do
factory :company do
sequence(:code) { |n| n + 1000 }
end
factory :quarter_value do
company
end
end
You need to modify your code to look like this:
let(:company) { Company.create }
describe 'company has many quarter values' do
before do
10.times { create(:quarter_value, company: company) }
company.reload
end
it 'has 10 quarter values' do
expect(company.quarter_values.count).to eq(10)
end
end
The company variable you created at the start has no knowledge that it has been given any quarter_values.
You need to call company.reload to update company with the new relations it was given because that instance of the Company model wasn't involved in create(:quarter_value, company: company)
You should reload the company object in either before block or inside the pry session, while debugging.
It
Reloads the attributes of this object from the database.
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
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.