rspec-core: Inherit and extend let definitions in inner describe blocks.

I’ve taken to an idiom like the following:

describe Post do
  subject { Post.new(post_attributes) }
  let(:post_attributes) { { } }

  its(:description) { should be_empty }

  describe "with a title" do
    let(:post_attributes) { { title: "10 things about yak shaving you're doing wrong" } }

    its(:description) { should == "10 things about yak shaving you're doing wrong" }

    describe "with an author" do
      let(:post_attributes) { { title: "10 things about yak shaving you're doing wrong", author: "Ernest Holbrecht" } }

      its(:description) { should == "10 things about yak shaving you're doing wrong by Ernest Holbrecht" }
    end
  end
end

The Post.new call is DRYed up into the top of the spec, and each nested describe block defined what’s different from its parent block. Except: here, the innermost describe block’s definition of post_attributes repeats the title from the one above it.

I’d prefer to say something like this:

context "with an author" do
  let(:post_attributes) { super.merge( author: "Ernest Holbrecht" ) }

  its(:description) { should == "10 things about yak shaving you're doing wrong by Ernest Holbrecht" }
end

Unfortunately, I don’t think we can use super, it would have to be super() (which is uglier) or else we’d get the error “Implicit argument passing of super from method defined by define_method() is not supported. Specify all arguments explicitly.”

So this issue is here to decide two things:

  1. Is this a good thing to add to RSpec?
  2. If so, what’s the right word to use to refer to the inherited value of a let?

I’m happy to write the implementation.

About this issue

  • Original URL
  • State: closed
  • Created 13 years ago
  • Comments: 26 (7 by maintainers)

Most upvoted comments

TLDR;

context 'when overriding let in a nested context' do
  let(:a_value) { super() + " (modified)" }

  it 'can use `super` to reference the parent context value' do
    expect(a_value).to eq("a string (modified)")
  end
end

Only found this neat feature so many years later ❤️

I stumbled into an error when using super instead of super(), but It is nicely explained here: https://rspec.info/blog/2013/02/rspec-2-13-is-released/#let-and-subject-declarations-can-use-super

Nice. I found this topic in 2019. 👍 Still working for RSpec 3.8.

Actually it does not make sense for LetOverride to perform the super.tap. I think the least surprising, and more flexible solution is to to just perform super instead.

module RSpec::LetOverride
  def let_override(name, &block)
    define_method(name) do
      if defined?(super)
        __memoized.fetch(name) { instance_exec(super(), &block) }
      else
        raise NoMethodError, "let #{name.inspect} not defined in a parent ExampleGroup."
      end
    end
  end
end

RSpec::Core::ExampleGroup.module_eval do
  extend RSpec::LetOverride
end

Which allows:

describe "foo" do
  let(:foo) { "origin foo" }
  context "overridden" do
    let_override(:foo) { |v| "#{v} overriden" }
    it "allows overriding with a reference to super" do
      foo.should == "origin foo overridden"
    end
  end
end

I was thinking about the same feature but in subject method… and what do you think about extra argument for block, something like:


let(:post_attribute) {|parent|  parent.merge( author: "Ernest Holbrecht"  }  
# or in subject 
describe Object do
  subject { Factory :object }
  ...
  describe "#attributes" do
    subject {|parent| parent.attributes }
    ...

You could avoid method name problems this way 😉