TDD has_many through model validation using RSpec & Factory Girl

Consider the following:

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

Note:

  • ScheduledSession will permanently exist in the system; think of it as a class or course.
  • The goal is to check the ApplicantSignup model for an attribute in a 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

RESTful resources i.e. the #create action will have a path 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 will notice that I set the virtual attribute above @session.duration to prevent the session from being considered invalid. The real β€œmagic” if you find yourself in @signup = ApplicantSignup.new(params[:signup].merge(:sessions => [@session])) , which now means that in the model I can choose from self.scheduled_sessions and get access to the ScheduledSession with which this ApplicantSignup is built, although at this point in time there is no record present in the connection table.

Model confirmations, 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 

This works well in development , and the checks work as expected - but how do I test with Factory Girl? I want unit tests to guarantee the logical logic that I implemented after all - Of course, this is after the fact, but still remains one way to switch to TDD.

You will notice that I got a comment raise StandardError, self.scheduled_sessions.inspect in the last check above - this returns [] for self.scheduled_sessions , which indicates that my Factory installation is not true.

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 that it correctly checks the model of the applicant_signup; the fact that self.scheduled_sessions returns [] just means that the above is incorrect.

Is it possible that I'm trying to test something outside the scope of Factory Girl, or is there a much better approach to solving this? Appreciate all comments, advice and constructive criticism!

Update:

  • Not sure what this is called, but this is an approach, at least with regard to how it is implemented at the controller level
  • I need to consider ignoring Factory Girl for the association aspect and try to return scheduled_session by mocking scheduled_sessions on the applicant_signup model.

Plants

 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 
+4
source share
2 answers

My previous assumption was correct, with a slight change:

I need to consider ignoring Factory Girl for the association aspect in and try to return sched_session using stubbing schedule_sessions on the applicant_signup model.

makes my tests pretty simple:

 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 duration of the test, as spork triggers a false message about it.

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

As another approach, you can use rspecs stub_model .

In addition, if you are testing ApplicantSignup , you must initialize it and not test the creation of Applicant . For instance:

 applicant_signup = Factory.build(:applicant_signup); applicant_signup.should_receive(:scheduled_sessions) .and_return{[scheduled_sessiβ€Œβ€‹on]}; 

Thus, there will be less access to the database, and you will test ApplicantSignup, not the Applicant.

0
source

Source: https://habr.com/ru/post/1380852/


All Articles