Alex Harvey
Alex Harvey

Reputation: 15472

Using a Helpers module to declare contexts in Rspec

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.)

Upvotes: 1

Views: 859

Answers (2)

Rimian
Rimian

Reputation: 38418

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.

Upvotes: 1

Alex Harvey
Alex Harvey

Reputation: 15472

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.

Upvotes: 1

Related Questions