I am facing a monster of issue on rspec, where each time I try to solve a bug it creates another.
I have a model Deal which has_many Steps
model/step.rb
belongs_to :deal, :foreign_key => 'deal_id'
model/deal.rb
has_many :steps, dependent: :destroy do
# added to enable maximum nb of steps a deal can have
# to take into account in active admin
# in order to calculate the correct new nb of step to compare to the authorized limit
# source: homeonrails.com/2012/10/validating-nested-associations-in-rails/
def length
reject(&:marked_for_destruction?).length
end
end
The gist of all my bugs is how to make one of my Feature rspec test work, where I associate Deals and Steps. I used to use Factory Girl "usual" transient which would be much cleaner but I had to move from it (see further below), because we have special requirements:
factory :deal_with_associated_steps do
to_create {|instance| instance.save(validate: false) } # skip validate
transient do
steps_count 27
end
after(:create) do |deal, evaluator|
create_list(:steps, evaluator.steps_count, deal: deal)
end
end
The reason I moved away from using this "transient" technique to create multiple Steps related to a Deal is very specific to our app.
When you take for a given Deal all the associated Steps 's st_appearance_order_nb ( integer) it must always be: - start from 0 - then have no gap so 1, 2,3...
Some before_validations on the Deal model Deals allow me to ensure this is always this case. You can't have a Deal with associated Steps where one of the Steps has a appearance_nb of 1 and another Step has a appearance_nb of 3 but there is no Step with appearance_nb 2. And you can't not have a step with a appearance_nb of 0. It simply must be a series 0,1,2,3...and so on
This would actually still work with the "classic transient" way to create Steps in Factory Girl. BUT the issue with the classic "transient" way above to create Steps is that I have a gem called "rspec-retry" that help me re-do the feature tests as, as many other rspec users with complex UI/javascript pages, sometimes my front end test fail the first time for some js/loading reason then if you repeat them enough, the 2nd or 3rd time it will work. So most of my feature tests are run 4 times, and some only pass the 2nd or 3rd time:) The gem rspec-retry is neat but had a very significant issue with the "transient" way I was creating Steps associated with in Factory Girl:
I was getting errors because if a test fail one the first "retry", the second time, it's like the test app thinks the st_appearance_order_nb nb 0 to 4 are already taken (indeed they were created by the first rspec "try"), so it creates now 4 new Steps with respectively st_appearance_order_nb of 5, 6, 7 and 8.
then ... leading to errors, because I have a before_validation to ensure that st_appearance_order_nb of a Deal's associated Steps are always starting at 0 then increment one by one
So using rspec-retry, I can't use Factory Girl transient way of creating associated Steps,at least that was my conclusion at the time where I found another way: I decided to "manually" create the associated Steps this way
let!(:deal_with_videos) { create(:deal,
title: "title deal 1" ) }
video_urls = [ "", # no video allowed on first step
"https://www.facebook.com/418234631906607/videos/495053617558041", # square video (5 sec)
"https://www.facebook.com/pili.morillo.56/videos/352355988613922", # landscape video with internal black sidebars
"https://www.facebook.com/rihanna/videos/10155330511896676/", # landscape video no internal black sidebars
"https://www.facebook.com/videos/1592424637461205/", # portrait video
""
]
(0..5).each do |n|
let!(:"deal_with_videos_step#{n}") {
FactoryGirl.create(:step,
st_appearance_order_nb: n,
st_video_url: video_urls[n],
deal: deal_with_videos)
}
end
this fixed the errors and 99% of my tests worked but now the issue of the present post: one of my test is failing : because very weirdly the way I associate Deal and Steps is not completely working, but only partly... Let me add everything is working fine in Production and development mode.
describe "on Deal Page load, the view behaves appropriately in terms of video" do
let(:action) { visit actual_deal_page_path(deal_with_videos) }
let(:fb_player_visibility) { "hidden" }
let(:video_href_set_by_app_js) { nil.to_s }
it_behaves_like "a view where the FB video player behaves appropriately"To be clear I found a very hack way to do stuff, but it created another issue in a ripple effect a new bug as the way I was doing it was making the test suite think there was no new
The test is failing for a reason I know now: It fails because the content of the following before_validation never gets executed
models/deal.rb
before_validation :extract_st_embed_hostings_from_st_video_urls
def extract_st_embed_hostings_from_st_video_urls
puts "beacon1"
self.steps.reject(&:marked_for_destruction?).each do |step|
puts "beacon2"
# do stuff
end
end
I know because this is the issue in a test environment with those puts message, when I run rspec test on the test block, I only see "beacon1" , and not beacon2 (in dev and prod both messages are seen)
I wondered why it was not being executed.
So I added inside the test some puts
to see why the line self.steps.reject(&:marked_for_destruction?).each do |step|
was not outputting anything. Could my association of Deal and Steps not work in the tests?
describe "on Deal Page load, the iew behaves appropriately in terms of video" do
before do
action
puts deal_with_videos.steps.to_json
puts deal_with_videos.steps[1].to_json
puts deal_with_videos.id
puts deal_with_videos_step0.deal_id
puts deal_with_videos_step0.deal.title
puts deal_with_videos_step0.to_json
end
let(:action) { visit actual_deal_page_path(deal_with_videos) }
let(:fb_player_visibility) { "hidden" }
it_behaves_like "a view where the FB video player behaves appropriately"
end
And the results are weird:
"puts deal_with_videos.steps.to_json" gives me [] => so it seems they're not associated
"puts deal_with_videos.steps1.to_json" gives "null" so consistent with the previous
puts
But the netx 2 puts bring more confusion:
"puts deal_with_videos.id" gives 3
"puts deal_with_videos_step0.deal_id" also give me 3
So going in the 2 directions, I have the same information, that's weird: it seems they are actually well associated. Weird as it contradicts for me the first 2 puts.
- but then the last "puts" give me:
puts deal_with_videos_step0.deal.title give sme "title deal1"
puts deal_with_videos_step0.to_json gives me a detailed json with content inside (not copied here to stay concise) => they both work
My conclusions
it's like the way i assciated them only works ONE way : if I start by going on a Step like deal_with_videos_step0, then move by using .deal to reach the Deal table, it works.
But in the other way around, the one that was in my before_validation called extract_st_embed_hostings_from_st_video_urls (see above) that is not working properly, it's not working: if I start on the table Deal then request all the Steps related to Deal, it does not work, it gives me empty outputs. so the request below is empty, that's why the before validation extract_st_embed_hostings_from_st_video_urls does not do anything, the test suite thinks there are no Steps to do stuff on.
So I'm blocked here: My issue is at the cross-roads of factory girl+rspec-retry+my specific Deal model's associated Steps attributes constraints
How can I associate in my tests a Deal and multiple Steps, while using rspec-retry and manage to get this test passing, that is to say by managing to have self.steps.reject(&:marked_for_destruction?).each "work" even inside the test environment, instead of thinking there is no "step" associated ?
EDIT
More information are provided here below following comments
1/ st_appearance_order_nb
- Creation
st_appearance_order_nb is just one attribute/column of a Step. Ut's added in Active Admin directly inside a Deal form via a has_many relation:
f.inputs "Steps" do
f.has_many :steps,
allow_destroy: true,
heading: false,
new_record: true,
# ensure each new step is automagically assigned a +1st_appearance_order_nb
sortable: :st_appearance_order_nb,
sortable_start: 0 do |step|
step.input :st_appearance_order_nb,
input_html: { readonly: true, disabled: true },
label: "Appearance rank"
step.input :st_video_url,
end
end
- Then intgerity on addition/removal...
models/deal.rb
before_validation :assign_new_st_appearance_order_nb_values_for_steps_in_case_of_steps_removals
before_validation :check_steps_start_on_zero
before_validation :check_steps_have_no_gap_for_st_appearance_order_nb
# in case one or more Steps are removed, avoid a "hole"
# in the st_appearance_order_nb due to those removals
# includes the other requirement to re-start the ranks at 0
def assign_new_st_appearance_order_nb_values_for_steps_in_case_of_steps_removals
if self.steps.any? && self.steps.select { |st| st.marked_for_destruction? }.any? # restrict this taxing operation to cases where there are removals
remaining_steps = self.steps.reject(&:marked_for_destruction?)
remaining_steps.sort_by(&:st_appearance_order_nb).each_with_index do |step, index|
step.update_attributes st_appearance_order_nb: index
end
end
end
def check_steps_start_on_zero
if self.steps.any?
if self.steps.map{|u| u.st_appearance_order_nb}.min != 0
errors[:base] << "Error on Steps. There must be at least one Step with Appearance rank equal to 0 ."
end
end
end
def check_steps_have_no_gap_for_st_appearance_order_nb
if self.steps.any?
if !array_one_steps_increment?( self.steps.map{|u| u.st_appearance_order_nb} )
errors[:base] << "Error on Steps: you can't have gaps inside the list of Appearance ranks. Only increments by one. Change the Appearance ranks accordindly."
end
end
end
def array_one_steps_increment?(array)
sorted = array.sort
lastNum = sorted[0]
sorted[1, sorted.count].each do |n|
if lastNum + 1 != n
return false
end
lastNum = n
end
true
end
EDIT
After days of searching with no success, kind of giving up but in a way that makes sense: indeed maybe so much difficulty comes form the fact I am testing this in Feature specs where actually I should not let the app callbacks "set itself" (via a set method) those attributes that are a problem (st_embed_hosting for example) so I opted to just mock them myself in Feature tests and do real testing to see if the callbacks work in Model specs. Hope it will be more consistent and working.