Skip to content

Apply module/shared context inclusion to individual examples

gitlab-qa-bot requested to merge more-powerful-include into master

Created by: myronmarston

This is an idea I'm toying with. I lean towards including it but I'm not totally sold on the idea yet.


### The problem

We have two different mechanisms for defining metadata-filtered shared context. On the configuration object:

RSpec.configure do |config|
  config.around(:example, :db_1) { |ex| DB.transaction(:always_rollback => true, &ex) }
end

...and as a shared context:

RSpec.shared_context "with a DB transaction", :db_2 do
  around(:example) { |ex| DB.transaction(:always_rollback => true, &ex) }
end

The latter is generally more useful because shared_context supports all rspec-core constructs, including let, helper methods, etc...where as the config object only provides the means to define hooks that way. So I'd consider the latter form to be generally "better" but it has a downside:

RSpec.describe "Group with both approaches applied", :db_1, :db_2 do
  # examples
end

RSpec.describe "Group with an example that has both approaches applied" do
  it "does stuff", :db_1, :db_2 do
  end
end

Both forms work fine in the first example (where the metadata has been applied to the group), but only the config-based approach (:db_1) works in the second example. The problem is that the shared_context is included (or not) in example groups, not individual examples, whereas the config can apply just fine. A user might reasonably expect the 2nd example to work, but as things currently stand, it won't. I myself have run into this a few times, where I had something that I had defined as a shared_context but thought I had defined on config, and I tagged an example and it didn't work.

Solution

I realized recently that ruby's object model (particularly the singleton_class) provides a means to very simply address this issue: we can include modules/shared contexts directly in the singleton class of the example group instance owned by an individual example. This allows the contents of the module or shared context to apply to one example, but not others in the group. I'm pleased with how simple the implementation was.

However, I'm not 100% sold on this, for a few reasons:

  • It's potentially surprising behavior for people that have a good mental model of example groups vs examples...those users probably wouldn't expect this to work.
  • It feels a little bit "magical" (even though in practice, it's actually really simple and is just leveraging ruby's object model).
  • It adds some additional processing to each example when it runs, potentially making RSpec a bit slower.
  • Including a module in an object's singleton class at run time busts the method cache, potentially having an even bigger affect on perf. (However, every time rspec-mocks features are used, the method cache gets busted since it has to do similar things all the time).
  • There's some slightly odd semantics with before(:context)/after(:context) hooks that are defined in the shared context. As currently implemented, when the context is applied to one example, these do not run. However, they potentially setup and teardown some state that other parts of the context (such as an around hook) use, so not running them could cause problems. I'm on the fence about whether or not they should be run.
  • This could be a breaking change for some folks. Although, I would not consider it a SemVer violation.
  • It's potentially confusing that config.extend modules aren't treated the same way. I didn't think it made sense to apply those modules the same, since they generally define class-level helper methods (e.g. macros) that are called from within a describe or context block. (But can anyone thing of a need or use to treat them the same way?)

TODO:

  • Get the build to pass on all rubies (I want to discuss this before putting effort into that).
  • Benchmark the perf difference this has.
  • Decide how to handle :context hooks.
  • Figure out how this should be documented.

Feedback wanted.

/cc @rspec/rspec

Merge request reports