has_many 的 TDD 通过 RSpec 和 RSpec 进行模型验证工厂女孩
请考虑以下事项:
ScheduledSession ------> Applicant <------ ApplicantSignup
注意事项:
- ScheduledSession 将始终存在于系统中;将其视为一门课程或课程。
- 这里的目的是在
signups_controller#create
期间根据 ScheduledSession 上的属性验证 ApplicantSignup 模型
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
资源是 RESTful,即 #create
操作将具有类似于 /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
您会注意到我在上面设置了一个虚拟属性@session.duration
以防止 Session 被视为无效。真正的“魔法”发生在 @signup = ApplicantSignup.new(params[:signup].merge(:sessions => [@session]))
中,这意味着在模型中我可以从 self.scheduled_sessions
中进行选择并访问此 ApplicantSignup 正在构建的 ScheduledSession,即使在此时,没有任何记录存在在连接表中。
例如,模型验证看起来像
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
上面的内容在开发
中运行得很好,并且验证按预期工作 - 但如何使用 Factory Girl 进行测试呢?我希望单元测试能够保证我已经实现的业务逻辑——当然,这是事后发生的,但仍然是 TDD 的一种方式。
您会注意到我在上面的最后一次验证中注释掉了 raise StandardError, self.scheduled_sessions.inspect
— 这将为 self.scheduled_sessions 返回
这表明我的工厂设置不正确。[]
众多尝试之一=)
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
第一个通过了,但老实说我不相信它正确验证了applicant_signup模型; self.scheduled_sessions
返回 []
的事实仅仅意味着上面的内容是不正确的。
我很可能正在尝试测试 Factory Girl 范围之外的东西,或者是否有更好的方法来解决这个问题?感谢所有评论、建议和建设性批评!
更新:
- 不确定这叫什么,但这是至少针对 如何在控制器级别实现
- 我需要考虑至少在关联方面忽略 Factory Girl 并尝试通过模拟返回
scheduled_session
applicant_signup
模型上的scheduled_sessions
。
工厂
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
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 mockingscheduled_sessions
on theapplicant_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
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
我之前的假设是正确的,只是做了一些小改动:
让我的测试变得非常简单:
这个测试特别需要类似的方法:
...并且成功 - 忽略测试持续时间,因为
spork
会导致错误报告。My earlier assumption was correct, with on small change:
making my tests quite simply:
and this test in particular required a similar approach:
...and success — ignore the testing duration as
spork
causes false reporting of this.作为另一种方法,您可以使用 Rspecs
stub_model
。另外,如果您测试
ApplicantSignup
,则应该对其进行初始化,而不是测试Applicant
的创建。例如:因此数据库访问会减少,您将测试ApplicantSignup,而不是Applicant。
As another approach you could use Rspecs
stub_model
.Also, if you test
ApplicantSignup
, you should init it and not test the creation of theApplicant
. Eg:So there will be less DB access and you will test ApplicantSignup, not Applicant.