1

I am trying to recursively declare contexts in Rspec from within a Helpers module.

(My code would be using these contexts in an unusual way, namely to recursively make assertions about keys in a nested Hash. Maybe I could solve this problem in a different way, but that's beside the point.)

A minimal complete example would be:

module Helpers
  def context_bar
    context "bar" do
      it "baz" do
        expect(true).to be true
      end
    end
  end
end

include Helpers

describe "foo" do
  Helpers.context_bar
end

If I now execute this code in Rspec it fails with:

RuntimeError:
  Creating an isolated context from within a context is not allowed. Change `RSpec.context` to `context` or move this to a top-level scope.

I can then refactor it as this:

def context_bar
  context "bar" do
    it "baz" do
      expect(true).to be true
    end
  end
end

describe "foo" do
  context_bar
end

And that works just fine for me, although I lose the benefit of the readability that comes with having this method and similar methods inside a module name space.

Is there any way for me to make this work?

(Note that there is a superficial similarity of this question to others like this one, or in the Rspec docs here. This seems to make Helpers available inside examples, but it won't allow me to actually declare a context.)

Alex Harvey
  • 14,494
  • 5
  • 61
  • 97
  • The example you provided would be probably better solved with shared examples: https://relishapp.com/rspec/rspec-core/v/3-7/docs/example-groups/shared-examples Do you have reasons not to use it? – Greg Aug 30 '18 at 12:30
  • No reason, if it's possible to do it this way. – Alex Harvey Aug 30 '18 at 12:57
  • 1
    Ok, I thought it might be like an intelectual challenge ;) In such case - shared examples are the way to go - they're totally a way to extract repeatable contexts and examples. – Greg Aug 30 '18 at 14:16
  • Thank you, then I will have a look into it. – Alex Harvey Aug 30 '18 at 15:45
  • Actually, it doesn't work. I then run into: `can't include shared examples recursively`. – Alex Harvey Sep 02 '18 at 09:45
  • Oh, I missed the `recursive` part. Can you share a code example that shows what you try to do with hashes? I think that maybe custom matcher could be better, but need to see a better example. – Greg Sep 03 '18 at 09:43
  • Sure, I updated with the code. – Alex Harvey Sep 03 '18 at 12:36

2 Answers2

1

As suggested in the comments, the answer here, in general, is to use Shared Examples. Thus, I can refactor this code example as:

RSpec.shared_examples "context_bar" do
  context "bar" do
    it "baz" do
      expect(true).to be true
    end
  end
end

describe "foo" do
  include_examples "context_bar"
end

If, however, I declare a recursive "function" like:

RSpec.shared_examples "compare" do |ref1, ref2|
  ...
end

and call it using:

include_examples "compare", ref1, ref2

This fails with:

ArgumentError:
  can't include shared examples recursively

See also shared examples in the docs.

It is also suggested in the comments that I could use a custom matcher. Indeed, someone did something very similar here.

Alex Harvey
  • 14,494
  • 5
  • 61
  • 97
1

You can add context in configuration. The rspec-graphql_response gem does a pretty nice job of it here. Though whether or not it is a good idea is another question.

Basically, it goes something like this:


require 'spec_helper'

# Put this in a helper somewhere.
RSpec.configure do |config|
  def my_context_helper(&block)
    block.call if block_given?
  end
end

RSpec.describe 'Smoke Test' do
  my_context_helper

  my_context_helper do
    # do something
  end
end

You can pass context around with instance variables. I think you'd need to be careful of collisions and tearing down your specs correctly. Else you might get unexpected results in your tests. Shared examples or shared context are probably safer, easier and uses the RSpec DSL as it is intended.

I also did a spike here.

Rimian
  • 36,864
  • 16
  • 117
  • 117