has_many 的 TDD 通过 RSpec 和 RSpec 进行模型验证工厂女孩

发布于 2024-12-14 22:19:16 字数 5336 浏览 7 评论 0原文

请考虑以下事项:

ScheduledSession ------> Applicant <------ ApplicantSignup

注意事项:

  1. ScheduledSession 将始终存在于系统中;将其视为一门课程或课程。
  2. 这里的目的是在 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:

  1. A ScheduledSession will exist in the system at all times; think of this as a class or course.
  2. 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 

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

回梦 2024-12-21 22:19:17

我之前的假设是正确的,只是做了一些小改动:

我需要考虑忽略 Factory Girl 的关联方面
至少并尝试通过存根返回scheduled_session
application_signup 模型上的 Scheduled_sessions。

让我的测试变得非常简单:

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

这个测试特别需要类似的方法:

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

...并且成功 - 忽略测试持续时间,因为 spork 会导致错误报告。

Finished in 2253.64 seconds
32 examples, 0 failures, 3 pending
Done.

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.
初雪 2024-12-21 22:19:17

作为另一种方法,您可以使用 Rspecs stub_model

另外,如果您测试 ApplicantSignup,则应该对其进行初始化,而不是测试 Applicant 的创建。例如:

applicant_signup = Factory.build(:applicant_signup);

applicant_signup.should_receive(:scheduled_sessions)
                           .and_return{[scheduled_sessi‌​on]};

因此数据库访问会减少,您将测试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 the Applicant. Eg:

applicant_signup = Factory.build(:applicant_signup);

applicant_signup.should_receive(:scheduled_sessions)
                           .and_return{[scheduled_sessi‌​on]};

So there will be less DB access and you will test ApplicantSignup, not Applicant.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文