2

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!

1
  • I can't say for sure but include_examples works only once per context. I think you have to surround it with a uniq context like context "...#{t}" do Commented Aug 10, 2016 at 10:23

1 Answer 1

3

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
Sign up to request clarification or add additional context in comments.

3 Comments

could you think of any reason why these loops would work individually (if I comment out the other) but not in combination? By work I mean the tests pass. But running everything results in some failing tests and tweaking the Hashes to leave out a few filter options would result in different tests failing. I brought the minimal required examples (to reproduce the failures) down to 50% (still ~350) and am running rspec --bisect on them, but that seems like it's going to take very long, after more than an hour I saw this - Remaining non-failing examples (337):
I don't see in the question, and I didn't suggest in my answer, anything that should have side effects that might break later tests. Unless you can think of something in these tests that changes global state, rspec --bisect and debugging the result seems like the way to go.
Found the problem, thought I'd let you know that it had nothing to do with what was discussed here, your suggestion works beautifully!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.