I'm trying to test a system for creating article translations where there is a self-join on the publications table. I've created a factory that will create multiple translations and associate them with a 'parent' article.
Using Rails 5 with factory_girl 4.7.0, rspec, and Database_cleaner
All actions work as expected, but creating a test is the problem
Here's the relevant model validations and methods:
# models/publication.rb
has_many :translations, class_name: "Publication", foreign_key: "translation_id", dependent: :nullify
belongs_to :translation, class_name: "Publication", optional: true
validates :language, uniqueness: { scope: :translation_id }, if: :is_translation?
def is_translation?
!translation.nil?
end
Factory (irrelevant code omitted):
# spec/factories/publication.rb
factory :publication, aliases: [:published_pub] do
title 'Default Title'
language 'EN'
published
after(:build) do |object|
create(:version, publication: object)
end
#-- This is where I suspect the problem stems from
trait :with_translations do
association :user, factory: :random_user
after(:build) do |object|
create_list(:translation, 3, {user: object.user, translation:object})
end
end
end
factory :translation, class: Publication do
sequence(:title) { |n| ['French Article', 'Spanish Article', 'German Article', 'Chinese Article'][n]}
sequence(:language) { |n| ['FR', 'ES', 'DE', 'CN'][n]}
user
end
And a basic test:
# spec/models/publication_spec.rb
before(:each) do
#translation_parent = create(:publication, :with_translations)
#pub_without_trans = create(:publication, :with_random_user)
end
scenario 'is_translation?' do
# No actual test code needed, this passes regardless
end
scenario 'has_translations?' do
# No actual test code needed, this (and subsequent tests) fail regardless
end
Finally, the error:
Failure/Error: create_list(:translation, 3, {user: object.user, translation:object})
ActiveRecord::RecordInvalid:
Validation failed: Language has already been taken
The first test passes (and the publication object with translations is created correctly)but any subsequent test fails. The issue is that I have a uniqueness validation scoped to translation_id and it appears that factorygirl is trying to add the generated translations to an already existing publication instead of creating an entirely new publication.
Any help is appreciated!
Solved!
The issue was that the sequence iterator in the translation factory was not resetting to 0 after each test. So after test 1, it started trying to access an array index that didn't exist. After it did that one more time, it triggered the validation and the tests failed!
The solution is not cute, but it's good enough for the time being
sequence(:language) do |iteration|
array = ['FR', 'ES', 'DE', 'CN']
# Returns a number between 0 and array.length
array[iteration%array.length]
end
sequence(:title) do |iteration|
array = ['French Article', 'Spanish Article', 'German Article', 'Chinese Article']
# Returns a number between 0 and array.length
array[iteration%array.length]
end
Related
I have model name: UserApplicationPatient.
That model having two associations:
belongs_to :patient
belongs_to :customer
before_create :set_defaults
private
def set_defaults
self.enrol_date_and_time = Time.now.utc
self.pap = '1'
self.flg = '1'
self.patient_num = "hos_#{patient_id}"
end
Factories of UserApplicationPatient
FactoryBot.define do
factory :user_application_patient do
association :patient
association :customer
before(:create) do |user_application_patient, evaluator|
FactoryBot.create(:patient)
FactoryBot.create(:customer)
end
end
end
Model spec:
require 'spec_helper'
describe UserApplicationPatient do
describe "required attributes" do
let!(:user_application_patient) { described_class.create }
it "return an error with all required attributes" do
expect(user_application_patient.errors.messages).to eq(
{ patient: ["must exist"],
customer: ["must exist"]
},
)
end
end
end
This is the first time I am writing specs of models.
Could someone please tell me how to write specs for set_defaults before_create methods and factories what I have written is correct or not.
Since you are setting default values in the before_create hook, I will recommend validating it like this
describe UserApplicationPatient do
describe "required attributes" do
let!(:user_application_patient) { described_class.create }
it "return an error with all required attributes" do
# it will validate if your default values are populating when you are creating a new object
expect(user_application_patient.pap).to eq('1')
end
end
end
To test your defaults are set, create a user and test if the defaults are set.
You definitely don't want to use let!, there's no need to create the object until you need it.
Since we're testing create there's no use for a let here at all.
How do we test Time.now? We can freeze time!
I assume patient_id should be patient.id.
Here's a first pass.
it 'will set default attributes on create' do
freeze_time do
# Time will be the same everywhere in this block.
uap = create(:user_application_patient)
expect(uap).to have_attributes(
enrol_date_and_time: Time.now.utc,
pap: '1',
flg: '1',
patient_num: "hos_#{uap.patient.id}"
)
end
end
it 'will not override existing attributes' do
uap_attributes = {
enrol_date_and_time: 2.days.ago,
pap: '23',
flg: '42',
patient_num: "hos_1234"
}
uap = create(:user_application_patient, **uap_attributes)
expect(uap).to have_attributes(**uap_attributes)
end
These will probably fail.
Defaults are set after validation has taken place.
Existing attributes are overwritten.
What is patient_id?
We can move setting defaults to before validation. That way the object can pass validation, and we can also see the object's attributes before writing it to the database.
We can fix set_defaults so it doesn't override existing attributes.
Time.now should not be used, it is not aware of time zones. Use Time.current. And there's no reason to pass in UTC, the database will store times as UTC and Rails will convert for you.
belongs_to :patient
belongs_to :customer
before_validation :set_defaults
private
def set_defaults
self.enrol_date_and_time ||= Time.current
self.pap ||= '1'
self.flg ||= '1'
self.patient_num ||= "hos_#{patient.id}"
end
We can also make your factory a bit more flexible.
FactoryBot.define do
factory :user_application_patient do
association :patient, strategy: :create
association :customer, strategy: :create
end
end
This way, the patient and customer will be created regardless whether you build(:user_application_patient) or create(:user_application_patient). This is necessary for user_application_patient to be able to reference its patient.id.
In general, don't do things at create time.
I have models in my Rails app:
Sales_Opportunity which has_many Swots.
I'm setting them up using FactoryGirl and running a test to show that when I delete my Sales_Opportunity I also cause the associated Swots to be deleted. For some reason when debugging with Byebug I'm getting strange results - the Sales_Opportunity and Swot records are being correctly created, but when I run sales_opportunity.swots it returns [ ] whereas sales_opportunity.swots.count returns 1. What's more odd is the exact same code works fine with anther object association (timeline_events are exactly like swots yet work perfectly with the same code).
Can anyone tell me what am I doing wrong please?
Sales_Opportunity.rb:
class SalesOpportunity < ActiveRecord::Base
default_scope { order('close_date ASC') }
belongs_to :user
belongs_to :company
has_many :key_contacts
has_many :timeline_events, dependent: :destroy
has_many :swots, dependent: :destroy
end
Swot.rb:
class Swot < ActiveRecord::Base
belongs_to :sales_opportunity
validates :swot_details, presence: true
validates :sales_opportunity_id, presence: true
enum swot_type: [:strength, :weakness, :opportunity, :threat]
enum swot_importance: { minimal: 1, marginal: 2, noteworthy: 3, significant: 4, critical: 5 }
validates :swot_importance, presence: true
end
Swot FactoryGirl spec:
FactoryGirl.define do
factory :swot do
swot_importance "minimal"
swot_details "Some boring details"
swot_type "threat"
trait :attached do
association :sales_opportunity, :with_user_id
end
end
end
Sales_Opportunity FactoryGirl spec:
FactoryGirl.define do
sequence(:opportunity_name) { |n| "Sales Oppotunity - #{n}" }
factory :sales_opportunity do
user
opportunity_name {generate(:opportunity_name)}
close_date "2014/12/12"
sale_value 10000
company_id 7
trait :with_user_id do
user_id 6
end
end
end
Failing Rspec tests:
describe "when swot's parent sales opportunity is destroyed" do
let(:swot) { FactoryGirl.create(:swot, :attached) }
let(:sales_opportunity) { swot.sales_opportunity }
it "should destroy associated swots" do
dswots = sales_opportunity.swots.to_a
byebug
sales_opportunity.destroy
expect(dswots).not_to be_empty
dswots.each do |dswot|
expect(Swot.where(id: dswot.id)).to be_empty
end
end
end
Output from the console (byebug) when logging swot:
#<Swot id: 13, swot_type: 3, swot_importance: 1, sales_opportunity_id: 564, swot_details: "Some boring details", created_at: "2015-07-27 10:57:23", updated_at: "2015-07-27 10:57:23">
Output from the console when logging sales_opportunity:
#<SalesOpportunity id: 564, close_date: "2014-12-12 00:00:00", user_id: 6, created_at: "2015-07-27 10:57:23", updated_at: "2015-07-27 10:57:23", pipeline_status: 0, opportunity_name: "Sales Oppotunity - 4", company_id: 7, sale_value: #<BigDecimal:7fe9ffd25078,'0.1E5',9(27)>, swot_score: 0>
Output for sales_opportunity.swots.count:
(byebug) sales_opportunity.swots.count
1
Output for sales_opportunity.swots:
(byebug) sales_opportunity.swots
#<ActiveRecord::Associations::CollectionProxy []>
I think I've included all the known info. The Rspec tests, FactoryGirl factories and setup between sales_opportunities and Swots/Timeline_Events is exactly the same - yet the Rspec tests pass for Timeline_Events and the collection_proxy works for those (so as far as I can tell, the code is identical):
Timeline_Event Factory:
FactoryGirl.define do
factory :timeline_event do
activity "Some activity"
due_date "2014/11/11"
trait :attached do
association :sales_opportunity, :with_user_id
end
end
end
Working Rspec tests:
describe "when sales opportunity is destroyed for timeline event" do
let(:timeline_event) { FactoryGirl.create(:timeline_event, :attached) }
let(:sales_opportunity) { timeline_event.sales_opportunity }
it "should destroy associated timeline events" do
timeline_events = sales_opportunity.timeline_events.to_a
sales_opportunity.destroy
expect(timeline_events).not_to be_empty
timeline_events.each do |event|
expect(TimelineEvent.where(id: event.id)).to be_empty
end
end
end
Timeline_Event.rb:
class TimelineEvent < ActiveRecord::Base
belongs_to :sales_opportunity
validates :activity, presence: true
validates :due_date, presence: true
validates :sales_opportunity_id, presence: true
end
When running byebug in the same place here I get an array including the Timeline_Event.
Can anyone help me understand what's going wrong in my code?
Thanks.
I solved the issue with this - it seems the sales_opportunity needs to be reloaded for the Active Record Associations to be persisted. This answer is the key to the solution.
Here's the working code:
describe "when swot's parent sales opportunity is destroyed" do
let!(:swot) { FactoryGirl.create(:swot, sales_opportunity: sales_opportunity) }
it "should destroy associated swots" do
sales_opportunity.reload
expect { sales_opportunity.destroy }.to change(sales_opportunity.swots, :count).by(-1)
end
end
Using some elements of Max's answer above also helped me improve the look and feel of the code.
RSpec.describe SalesOpportunity, type: :model do
let(:sales_opportunity) { create(:sales_opportunity) }
describe "swots" do
let!(:swot) { create(:swot, sales_opportunity: sales_opportunity) }
it "destroys nested swots" do
sales_opportunity.destroy
swot.reload
expect(swot.destroyed?).to be_truthy
end
end
end
Note that i'm adding this to the SalesOpportunity spec, because the dependant destroy behavior belongs to SalesOpportunity not the child association.
Edit.
Another way to write this spec would be:
it "destroys nested swots" do
expect {
sales_opportunity.destroy
}.to change(Swot, :count).by(-1)
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.
I am using Ruby on Rails 3.0.9, RSpec-rails 2 and FactoryGirl. I am trying to state a Factory association model but I am in trouble.
I have a factories/user.rb file like the following:
FactoryGirl.define do
factory :user, :class => User do
attribute_1
attribute_2
...
association :account, :factory => :users_account, :method => :build, :email => 'foo#bar.com'
end
end
and a factories/users/account.rb file like the following:
FactoryGirl.define do
factory :users_account, :class => Users::Account do
sequence(:email) {|n| "foo#{n}#bar.com" }
...
end
end
The above example works as expected in my spec files, but if in the factory :users_account statement I add the association :user code so to have
FactoryGirl.define do
factory :users_account, :class => Users::Account do
sequence(:email) {|n| "foo#{n}#bar.com" }
...
association :user
end
end
I get the following error:
Failure/Error: Unable to find matching line from backtrace
SystemStackError:
stack level too deep
How can I solve that problem so to access associated models from both sides\factories (that is, in my spec files I would like to use RoR association model methods like user.account and account.user)?
P.S.: I read the Factory Girl and has_one question and my case is very close to the case explained in the linked question. That is, I have an has_one association too (between User and Users::Account classes).
According to the docs, you can't just put both sides of the associations into the factories. You'll need to use their after callback to set an object(s) to return.
For instance, in the factories/users/account.rb file, you put something like
after(:build) do |user_account, evaluator|
user_account.user = FactoryGirl.build(:user, :account=>user_account)
end
For has_many associations, you'll need to use their *_list functions.
after(:build) do |user_account, evaluator|
user_account.users = FactoryGirl.build_list(:user, 5, :account=>user_account)
end
Note: I believe the example in the docs is a bit misleading it doesn't assign anything to the object. I believe it should be something like (note the assignment).
# the after(:create) yields two values; the user instance itself and the
# evaluator, which stores all values from the factory, including ignored
# attributes; `create_list`'s second argument is the number of records
# to create and we make sure the user is associated properly to the post
after(:create) do |user, evaluator|
user.posts = FactoryGirl.create_list(:post, evaluator.posts_count, user: user)
end
Spyle's excellent answer (still working with Rails 5.2 and RSpec 3.8) will work for most associations. I had a use case where a factory needed to use 2 different factories (or different traits) for a single has_many association (ie. for a scope type method).
What I ended up coming up with was:
# To build user with posts of category == 'Special' and category == 'Regular'
after(:create) do |user, evaluator|
array = []
array.push(FactoryBot.create_list(:post, 1, category: 'Regular')
array.push(FactoryBot.create_list(:post, 1, category: 'Special')
user.posts = array.flatten
end
This allowed the user to have 1 post of category 'Regular' and 1 post of category 'Special.'
I have written my basic models and defined their associations as well as the migrations to create the associated tables.
EDIT - Adding emphasis to what I specifically want to test.
I want to be able to test:
The associations are configured as intended
The table structures support the associations properly
I've written FG factories for all of my models in anticipation of having a complete set of test data but I can't grasp how to write a spec to test both belongs_to and has_many associations.
For example, given an Organization that has_many Users I want to be able to test that my sample Organization has a reference to my sample User.
Organization_Factory.rb:
Factory.define :boardofrec, :class => 'Organization' do |o|
o.name 'Board of Recreation'
o.address '115 Main Street'
o.city 'Smallville'
o.state 'New Jersey'
o.zip '01929'
end
Factory.define :boardofrec_with_users, :parent => :boardofrec do |o|
o.after_create do |org|
org.users = [Factory.create(:johnny, :organization => org)]
end
end
User_Factory.rb:
Factory.define :johnny, :class => 'User' do |u|
u.name 'Johnny B. Badd'
u.email 'jbadd#gmail.com'
u.password 'password'
u.org_admin true
u.site_admin false
u.association :organization, :factory => :boardofrec
end
Organization_spec.rb:
...
it "should have the user Johnny B. Badd" do
boardofrec_with_users = Factory.create(:boardofrec_with_users)
boardofrec_with_users.users.should include(Factory.create(:johnny))
end
...
This example fails because the Organization.users list and the comparison User :johnny are separate instances of the same Factory.
I realize this doesn't follow the BDD ideas behind what these plugins (FG, rspec) seemed to be geared for but seeing as this is my first rails application I'm uncomfortable moving forward without knowing that I've configured my associations and table structures properly.
Your user factory already creates an organization by virtue of the Factory Girl association method:
it "should associate a user with an organization" do
user = Factory.create(:johnny)
user.organization.name.should == 'Board of Recreation'
organization = user.organization
organization.users.count.should == 1
end
Take a look at 'log/test.log' after running your spec -- you should see an INSERT for both the organization and the user.
If you wanted to test this without the Factory Girl association, make a factory that just creates the user and make the association in the spec:
it "should associate a user with an organization" do
user = Factory.create(:johnny_no_org)
org = Factory.create(:boardofrec)
org.users.should be_empty
org.users << user
org.users.should include(user)
end
Of course all this is doing is testing whether ActiveRecord is doing its job. Since ActiveRecord is already thoroughly tested, you'll want to concentrate on testing the functionality of your application, once you've convinced yourself that the framework actually does what it's supposed to do.