elefish elefish - 3 months ago 14
Ruby Question

Using a loop variable in a let in a shared example call

I'm writing specs for a PDF generated with RoR & Prawn. There are a bunch of filter options (24 of them) for that PDF. In order to not miss any important specs I'm generating the filter options in a

before :context
block and saving them in an instance variable.

Where my problem starts is when trying to iterate over all of the filter options and run shared examples, to test for basics that don't change much with different filters.

This is what my code looks like for all these filters and the documentations filter (the
filter_setting
method is just a helper to access specific
@major_filter_options
):

describe 'pdf' do
before :context do
@major_filter_options = {}
@major_filter_options.define_them
end

describe 'basic content' do
before :context do
@filter_options_with_docu = {}
# find all the filters that have the docu option enabled
@major_filter_options.each do |key, mfo|
@filter_options_with_docu[key] = mfo if key.to_s.include? 'docu'
end
end

24.times do |t| # can't access major_filter_options.size here.. it's nil.
include_examples 'first_3_pages' do
let(:pdf) do
filter_options = filter_setting(@major_filter_options.keys[t])
ProjectReport::ReportGenerator.new.generate(project, filter_options, user).render
end
let(:page_analysis) { PDF::Inspector::Page.analyze(pdf) }
end
end

12.times do |t| # @print_options_with_docu is also nil at this point
include_examples 'documentation_content' do
let(:pdf) do
filter_options = filter_setting(@filter_options_with_docu.keys[t])
ProjectReport::ReportGenerator.new.generate(project, filter_options, user).render
end
let(:page_analysis) { PDF::Inspector::Page.analyze(pdf) }
end
end
# ...
end


I have 2 big problems:

One is that this
24.times
,
12.times
and so on (there's a bunch of them) are bothering me because it makes maintainance a lot harder. A new filter option would change all the values, and finding all of the values to change them is very susceptible to mistakes in my opinion.

The other problem is the fact that the variable iterated here like this:
12.times do |t|
doesn't actually seem to iterate when I'm inside of any of these
let
's:

let(:pdf) do
filter_options = filter_setting(@major_filter_options.keys[t])
puts t
# ...
end


The
puts t
here will print 11 every time (the filter is also the same every time).
After some reading I found a gist example. The problem looked similar enough, but sadly puting a
describe
block around it didn't do much.

24.times do |t|
describe
# same as before
end
end


Interestingly enough though, when doing
puts t
again in that setup, it would be 6 every time, which left me a little more confused.

I should also mention, that the reason for splitting them up like this is that I have shared examples that only apply for certain filters. If someone has a better idea on how to, for example iterate over the
@major_filter_options
and then just call certain shared examples depending on the current hash
key
, I'm all ears!

Answer

Regarding your inability to call .times on the instance variables you defined in before :context blocks:

RSpec works in two phases. In the first phase it executes the Ruby code in the spec file; the let and before and it methods store their blocks to be run later. In the second phase it actually runs the tests, i.e. the contents of let and before and it blocks. The before :context blocks don't define thost instance variables until the second phase, so the .times statements, which run in the first phase, can't see the instance variables.

The solution is to put the filter options someplace that's initialized before RSpec gets to the .times statements, like a constant.

Regarding include_examples in a loop always using the same value of the loop variable:

include_examples includes the given shared examples in the current context. If you include the same examples more than once, the examples themselves will be included multiple times, but lets in the last inclusion will overwrite lets in all of the previous inclusions. The RSpec documentation has a clear example.

The solution is to use it_behaves_like instead of include_examples. it_behaves_like puts the included examples in a nested example group, so the lets can't overwrite one another.

Applying those two solutions gives something like the following:

describe 'pdf' do
  describe 'basic content' do
    MAJOR_FILTER_OPTIONS = # code that initializes them
    MAJOR_FILTER_OPTIONS.values.each do |filter_option|
      it_behaves_like 'first_3_pages' do
        let(:pdf) do
          filter_options = filter_setting(filter_option)
          ProjectReport::ReportGenerator.new.generate(project, filter_options, user).render
        end
        let(:page_analysis) { PDF::Inspector::Page.analyze(pdf) }
      end
    end

    FILTER_OPTIONS_WITH_DOCU = # code that chooses them from MAJOR_FILTER_OPTIONS
    FILTER_OPTIONS_WITH_DOCU.values.each do |filter_option|
      it_behaves_like 'documentation_content' do
        let(:pdf) do
          filter_options = filter_setting(filter_option)
          ProjectReport::ReportGenerator.new.generate(project, filter_options, user).render
        end
        let(:page_analysis) { PDF::Inspector::Page.analyze(pdf) }
      end
    end

  end
end