3

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.

Mathieu
  • 4,587
  • 11
  • 57
  • 112
  • Wow, what a journey :) – Sergio Tulentsev Jun 26 '18 at 16:27
  • `:foreign_key => 'deal_id'` - redundant. It's `deal_id` by default. – Sergio Tulentsev Jun 26 '18 at 16:28
  • 1
    Woah woah woah, ok... Breathe... This whole issue seems to stem from the handling of `steps.st_appearance_order_nb` (which is a really bad column name, to be honest... why not call it something like `steps.position`?). It looks like you've gotten tangled up in a huge mess to workaround that design flaw. – Tom Lord Jun 26 '18 at 16:30
  • 1
    Forget everything else for a moment, can we just focus on this? Where does `st_appearance_order_nb` get set? Where does it get validated? How do you maintain data integrity, e.g. if a `step` gets destroyed? – Tom Lord Jun 26 '18 at 16:32
  • Internets say that that custom method `length` on the association is actually never used: https://stackoverflow.com/questions/4836897/validate-the-number-of-has-many-items-in-ruby-on-rails#comment81093646_28476834 – Sergio Tulentsev Jun 26 '18 at 16:34
  • 1
    Moreover, In fact, can we just see the full Deal <--> Step association? I think your whole problem is all around this coupling, and maintaining data integrity. – Tom Lord Jun 26 '18 at 16:36
  • 1
    @SergioTulentsev yes it's a journey but my differents changes in the way i test/create asscoiated Steps had to be reminded to explain the current issue:) – Mathieu Jun 26 '18 at 16:45
  • @TomLord yes steps.st_appearance_order_nb is a bit "heavy" and position would be abetter name but this did not cause the issue :) – Mathieu Jun 26 '18 at 16:46
  • @TomLord I am going to add how st_appearance_order_nbgets set, validated and keep integrity inc ase sb removes/add new Steps inside our active admin backoffcie – Mathieu Jun 26 '18 at 16:47
  • @TomLord just added details on st_appearance_order_nb: most is maintained by error messages and blocks inside Active Admin if they're not correct / don't comply with our validations – Mathieu Jun 26 '18 at 16:56
  • Maybe not a big deal, but `check_steps_start_on_zero` and `check_steps_have_no_gap_for_st_appearance_order_nb` are not `before_validation` material. They're actual validations. And the persisting/saving `update_attributes` in `assign_new_st_appearance_order_nb_values_for_steps_in_case_of_steps_removals`? `before_validation` shouldn't do that. – Sergio Tulentsev Jun 26 '18 at 16:59
  • @SergioTulentsev "internets say that that custom method length on the association is actually never used" => really strange because it really save me: this was really buggy and not related to the issue of this post but my validation was "a deal can't have more tha 10 steps", and when creating Steps directly inside Active Admin Deal Form (see code added in my edit), it was not working at all as new blocks added or those deleted were not being take into account (rspectively added or substracted) to compare with the limit of 10 steps. this tehcnique really fixed this and now it works. – Mathieu Jun 26 '18 at 16:59
  • @SergioTulentsev "Maybe not a big deal, but check_steps_start_on_zero and check_steps_have_no_gap_for_st_appearance_order_nb are not before_validation material. They're actual validations."=> true but I am not sure I remember why i did this: basically it must have been about some question of having an order of execution multiple for before_validations that you can't ensure for pure "validates"; not sure about this to be honest – Mathieu Jun 26 '18 at 17:01
  • I assume you copy/pasted from your code directly? If that's the case, doesn't your code crash in some way? Because you have `remaining__steps` and `remaining_steps` next to each other. – Sergio Tulentsev Jun 26 '18 at 17:04
  • @SergioTulentsev no I ad modified it a little to reduce the length of the name attributes haha so it's a typo __ was in fact _. just edited it. You've got the eye of a hawk hehe. As i said there is no crash at all in dev/prod and i even tested false positive and made sure with "put"s that all the before_validations content get executed in dev/prod and they are. the only place where they don't get executed is in test environment. – Mathieu Jun 26 '18 at 17:06
  • @TomLord "Moreover, In fact, can we just see the full Deal <--> Step association? problem is all around this coupling, and maintaining data integrity. " => agree! the experiments with the "puts" msgs definitely show this. But in dev/produ all is being executed perfectly. So it boils down to how i associate them in tests. I'd rather use Factory Girl transient "usual" way as explained but I could not due to rspec-retry (I clean the database between tests but it seems that the database does not get cleaned between rspec retries hence the issue with keeping factory girls AND rspec-retry) – Mathieu Jun 26 '18 at 17:09

0 Answers0