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.
Related
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
we defined the following model
class UserPool < ActiveRecord::Base
belongs_to :pool
belongs_to :user
validates :pool, presence: true
validates :user, presence: true
def self.created(date)
where("DATE(created_at) = ?", date)
end
end
and the following Factroy
FactoryGirl.define do
factory :user_pool do
pool
user
factory :paid_user_pool do
money_on_pool 10
end
end
end
When I run the following test I recive an error
describe "obtain users_pools created at specifict time" do
before do
users = create_list :user, 3, active: false
user4 = create :user
#pool = create :pool
users.each do |user|
create :user_pool, user: user, pool: #pool, created_at: 1.days.ago
end
create :user_pool, user: user4, pool: #pool
end
it "should return just users_pools created at specifict time" do
users_pools = #pool.user_pools.created( 1.days.ago )
users_pools.count.should eq 3
end
end
Error:
ActiveRecord::RecordInvalid:
The validation failed: Pool can’t be blank
Why is my factory not recognizing my pool association?
When creating a factory, you list the attributes with predefined values. Otherwise, you can omit them from the factory and explicitly state it in the test (during create).
# Example for :user
factory :user do
sequence(:name) { |n| "Test User #{n}" }
end
Now, when you call create(:user), the default name will include a number that increases by 1 for every user created. See #sequence and "Sequences" for more information.
Now onto your specific example. You can create the user_pool factory one of two ways.
# No default attributes, requires explicit assignment
factory :user_pool do
end
create(:user_pool, user: user, pool: #pool)
# Default attributes can be overridden during test
# Requires you to create :user and :pool factories
factory :user_pool do
after(:build) do |user_pool|
user_pool.user = create(:user)
user_pool.pool = create(:pool)
end
end
When you build an ActiveRecord object, it is not committed to the database. You are allowed to leave out required attributes. After building the object, two are created (user, pool), and assigned to the correct user_pool attributes. See "Callbacks" in the docs for more information.
If you want to create #pool in your test, you can still do the following. It will override the default pool and user attributes.
user_pool = create(:user_pool, user: user, pool: #pool)
I have this piece of example to create a new record through post which is passed
describe 'POST create' do
let(:schedule_child) { FactoryGirl.create(:schedule_child) }
let(:post_queue) { post :create, schedule_child_id: schedule_child.id, format: :js }
it { expect{post_queue}.to change(PatientQueue, :count).by(1) }
end
And I have one attribute, PatientQueue.queue_number, which will be increased by 1 every time a new record is added. Now I'd like to see if this attributes has changed.
it { expect{post_queue}.to change(PatientQueue, :queue_number).by(1) }
But here is what I got
NoMethodError: undefined method `queue_number' for #<Class:0x0000000849e780>
How should I write it properly?
== UPDATE ==
model PatientQueue
class PatientQueue < ActiveRecord::Base
# Validations
validates :patient, :schedule_child, presence: true
validate :is_not_exist
# Relations
belongs_to :schedule_child
belongs_to :patient
before_create :insert_queue_number
def is_exist?
PatientQueue.find_by_schedule_child_id_and_patient_id(schedule_child_id, patient_id).present?
end
private
def insert_queue_number
last_id = PatientQueue.where("schedule_child_id = ?", self.schedule_child_id).count
self.queue_number = last_id + 1
end
def is_not_exist
errors.add(:schedule_child, :is_exist) if is_exist?
end
end
PatientQueue is an activerecord class, which has a method count
post_queue is an instance of the class and has the method queue_number
the class does not have the same methods as the instance, so you might write your test like change(post_queue, :queue_number).by(1)
However, the test is a little hard to follow, can you show us your data model relationships? if a PatientQueue has_many schedule_child, maybe you just want to use rails cache_counter? http://www.elegantruby.com/Tutorials/2013/01/25/adding-a-counter-cache-for-fun-and-profit/
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'm having some problems testing StateMachines with Factory Girl. it looks like it's down to the way Factory Girl initializes the objects.
Am I missing something, or is this not as easy as it should be?
class Car < ActiveRecord::Base
attr_accessor :stolen # This would be an ActiveRecord attribute
state_machine :initial => lambda { |object| object.stolen ? :moving : :parked } do
state :parked, :moving
end
end
Factory.define :car do |f|
end
So, the initial state depends on whether the stolen attribute is set during initialization. This seems to work fine, because ActiveRecord sets attributes as part of its initializer:
Car.new(:stolen => true)
## Broadly equivalent to
car = Car.new do |c|
c.attributes = {:stolen => true}
end
car.initialize_state # StateMachine calls this at the end of the main initializer
assert_equal car.state, 'moving'
However because Factory Girl initializes the object before individually setting its overrides (see factory_girl/proxy/build.rb), that means the flow is more like:
Factory(:car, :stolen => true)
## Broadly equivalent to
car = Car.new
car.initialize_state # StateMachine calls this at the end of the main initializer
car.stolen = true
assert_equal car.state, 'moving' # Fails, because the car wasn't 'stolen' when the state was initialized
You may be able to just add an after_build callback on your factory:
Factory.define :car do |c|
c.after_build { |car| car.initialize_state }
end
However, I don't think you should rely on setting your initial state in this way. It is very common to use ActiveRecord objects like FactoryGirl does (i.e. by calling c = Car.net; c.my_column = 123).
I suggest you allow your initial state to be nil. Then use an active record callback to set the state to to the desired value.
class Car < ActiveRecord::Base
attr_accessor :stolen # This would be an ActiveRecord attribute
state_machine do
state :parked, :moving
end
before_validation :set_initial_state, :on => :create
validates :state, :presence => true
private
def set_initial_state
self.state ||= stolen ? :moving : :parked
end
end
I think this will give you more predictable results.
One caveat is that working with unsaved Car objects will be difficult because the state won't be set yet.
Tried phylae's answer, found that new FactoryGirl does not accept this syntax, and after_build method does not exists on ActiveRecord object. This new syntax should work:
Factory.define
factory :car do
after(:build) do |car|
car.initialize_state
end
end
end