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

Created on 29 Jan 2011  路  26Comments  路  Source: rspec/rspec-core

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.

Most helpful comment

Nice. I found this topic in 2019. :+1: Still working for RSpec 3.8.

All 26 comments

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

Yes, I think the subject wants the same thing. The extra argument idea's clever. It feels a little noisy, but it might be the safest thing to do.

On one hand super makes sense because if you were defining these as methods you would expect to use super to call the parent example group's method. (Granted, this may not make sense to people new to RSpec.)

On the other hand, I would prefer the block parameter as @dnurzynski suggests. I like being able to name the argument as this could be used to remind the user what the parent actually is, or for the cases where it is obvious a simple one letter variable could be used.

So.. +1 on the _optional_ block argument.

I agree with the optional block parameter.
IMO it's a pretty unobtrusive way to add functionality, and I also think it would be intuitive and coherent with the current RSpec mindset.

I'm writing this specifically in response to: http://groups.google.com/group/rspec/browse_thread/thread/3e8b630b61f3e90c?pli=1

I am _not_ in favor of adding the optional block argument. -1.

I find myself using the idiom Peeja described in my specs often.

I've run into that problem of wanting to use "super" too, but I feel it breaks the elegance of let(). Here is how I would have written the original:

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

  its(:description) { should be_empty }

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

    its(:description) { should == title }

    context "and an author" do
      let(:author) { "Ernest Holbrecht" }
      let(:post_attributes) { { title: title, author: author } }

      its(:description) { should == "#{title} by #{author}" }
    end
  end
end

Please consider: do you really need to extend rspec core, or maybe some better way of organizing your let() makes for cleaner specs?

I've run into more complex needs that the above reorganization does not solve. I've solved those in these two ways:

(1) Factory pattern.

let(:something_factory) { lambda { |blah| foo.make } }

(2) Use adjectives

describe Comment do
  subject { comment }
  let(:resource) { comment }
  let(:comment) { Comment.make }

  context 'with moderated comment' do
    let(:resource) { moderated_comment }
    let(:moderated_comment) { comment.tap(&:moderate!) }

    it 'should blah blah blah'
  end
end

I find myself using (2) more and more often. Attaching adjectives to names for let() also makes the tests clearer. I've found that, taking a page from inherited_resources gem, wrapping them in generic names like "resource" and "collection" lets me change up which resource I am using. It also makes it easier to factor out into mini DSL extensions I drop into spec/support, like so:

describe MyWebService::Application do
  include SpecHelpers::Rack
  include SpecHelpers::Application
  let(:http_headers) { mobile_subdomain_http_headers }

  context '/challenges' do
    let(:resource) { challenge }

    request :get, '/challenges/1.json' do
      let(:request_url) { "/challenges/#{resource.id}.json" }
      expects_status(200)
      expects_content_type('application/json', 'utf-8')
    end
  end
end

@hosh: I think you're right: those are all good strategies. But I think there's still a use case for inheriting from earlier lets and subjects. Consider the post_params in this controller spec:

describe PostsController do
  describe "#create" do
    subject { post :create, post: post_params; response }

    context "for valid params" do
      let(:post_params) do
        {
          title: "RSpec community debates feature",
          author: "Michael Arrington"
          # Potentially many more...
        }
      end

      it { should assign(:post).as(an_instance_of(Post)) }

      context "with return_to set" do
        let(:return_to_url) { "/kansas" }
        let(:post_params) { super.merge(return_to: return_to_url) }
        it { should redirect_to(return_to_url) }
      end
    end
  end
end

Or consider the cascading subjects in this view spec:

describe "posts/_post.html" do
  let(:post) { Factory.create(:post) }

  subject do
    render "posts/post", post: post
    Nokogiri::HTML(rendered)
  end

  describe "div.post" do
    subject { super.at_css("##{dom_id(post)}") }

    it { should be }

    describe "title" do
      subject { super.at_css("h2.title") }

      it { should be }
      its(:text) { should include(post.title) }
    end

    describe "author" do
      subject { super.at_css("span.author") }

      it { should be }
      its(:text) { should include(post.author) }
    end
  end
end

I think that's pretty readable and elegant.

I find _super_ is more intuitive than the optional block parameter. I think the block parameter should be reserved for another feature request (allowing let methods take arguments). For me, seeing a #let blocks take a parameter makes me think I can pass arguments along in my examples (which would be helpful in certain situations), whereas super kind of already infers that you're inheriting something else (a block parameter does no such thing in typical usage).

When first reading this issue I cringed at the thought of even adding +super+, but I agree that this feature can improve examples in nested contexts/describes without degrading the ability for a developer to gracefully use rspec.

+1 to @Peeja's suggestion of super.
-1 to the block parameter.

By the way, I'm being a bit misleading when I use super in the above examples. As I laid out in the original post, it would have to be either super() or something_else that we decide.

I agree that the block param should be reserved for parameterization of methods defined using let. That makes much more sense to me than having it represent the super concept.

Not sold on the feature at all yet, but I'm still listening :)

@Peeja

I don't use controller specs or html view specs. Instead of controller specs, I test Rack endpoints directly by making calls to MyApplication::Application.call

Your examples require super because it is not structured correctly. For example, in the view test, you should be doing this:

describe "posts/_post.html" do
  subject { view }
  let(:post) { Factory.create(:post) }
  let(:view) do
    render "posts/post", post: post
    Nokogiri::HTML(rendered)
  end

  describe "div.post" do
    subject { div_post }
    let(:div_post) { view.at_css("##{dom_id(post)}") }

    it { should be }

    describe "title" do
      subject { title }
      let(:title) { div_post.at_css("h2.title') }

      it { should be }
      its(:text) { should include(post.title) }
    end

    describe "author" do
      subject { author }
      let(:author) { div_post.at_css('span.author') }

      it { should be }
      its(:text) { should include(post.author) }
    end
  end
end

The super looks shorter. I'm not sure it is clearer.

In any case, I've been rethinking the whole BDD/rspec thing anyways, based on a more powerful definition of "agile" than what you'd find in the Agile Manifesto. Looking at this, the super thing wouldn't really bother me since I don't have to use it. The use of block params to define inner and outer scopes would be much more intrusive -- I'd much rather use those to pass parameters directly (like for factories).

@hosh, I can agree with you on:

The super looks shorter. I'm not sure it is clearer... The use of block params to define inner and outer scopes would be much more intrusive

But the rest seems a little presumptuous. Perhaps peeja will be happy you told him the _right_ way to do things.

I was considering this today also, good to see it's already under discussion.

Recently, I'm doing a lot of:

describe BlahController do
  describe '#new' do
    subject { get :new }

    it { should be_successful }

    describe 'the decoded response' do
      def subject; ActiveSupport::JSON.decode super.body; end

      it { should be_present }
      it { should include "blah_thing" => "foo" }
    end
  end
end

This feels super-crufty and would be greatly improved by subject { ActiveSupport::JSON.decode super.body } and would also make a lot more sense.

This also adheres to the principle of ExampleGroups being classes which inherit from each other, subjects and let blocks could be define_methoded in the class and have easy access to super. It could also clean up this business. The tricky bit would be cross-runtime compatibility.

I might try putting together an example branch if I get some spare time.

@sj26 there are already rspec features to solve that problem:

describe BlahController do
  describe '#new' do
    subject { raw_body }
    let(:raw_body) { get :new }
    let(:response_body) { ActiveSupport::JSON.decode raw_body }

    it { should be_successful }

    describe 'the decoded response' do
      subject { response_body }

      it { should be_present }
      it { should include "blah_thing" => "foo" }
    end
  end
end

You can also do:

# spec/support/request_helpers.rb
module SpecHelpers
  module Requests
     include ActiveSupport::Concern

     included do
       let(:raw_body) { get :new }
       let(:response_body) { ActiveSupport::JSON.decode raw_body }
     end
  end
end

# spec/requests/blah_controller_spec.rb
require 'spec_helper'

describe BlahController do
  include SpecHelpers::Requests

  describe '#new' do
    subject { raw_body }

    it { should be_successful }

    describe 'the decoded response' do
      subject { response_body }

      it { should be_present }
      it { should include "blah_thing" => "foo" }
    end
  end
end

This way, you can share all the let() definitions across all of your controller specs.

Anybody here still fired up about this feature? To me, it feels like there are cleaner, more readable and readily understandable methods to achieve a similar thing rather than nested/chained/inherited lets.

I am! I just wrote a spec and found it would be nice to have inheritance in lets.

I tried calling super() within a let, and got the following error:

NotImplementedError: super from singleton method that is defined to multiple classes is not supported; this will be fixed in 1.9.3 or later

Using super has an issue where the code is invoked even after the first call. This means, you would need to have your own memoization.

I created a let_override method for my project, which overrides the method and works with memoization.

module RSpec::LetOverride
  def let_override(name, &block)
    define_method(name) do
      if defined?(super)
        __memoized.fetch(name) { super().tap {|o| instance_exec(o, &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

It works by having the block take an argument, which is the return value of the super implementation.

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

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 applied this to my codebase, and found that in all cases, I ended up mutating and/or running assertions on the super value and returning the super value.

So I made let_override do the super.tap and added a let_override!, which is the "dangerous" version of let_override, which allows you to define your own return value.

# See https://github.com/rspec/rspec-core/issues/294
module RSpec::LetOverride
  def let_override(name, &block)
    let_override!(name) do |value|
      instance_exec(value, &block)
      value
    end
  end

  def let_override!(name, &block)
    define_method(name) do
      if defined?(super)
        __memoized.fetch(name) { |k| __memoized[k] = 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

I also want to add that I'm making heavy usage of let_override to dry up the nested describes. This leads to cleaner, easier to read, less crufty specs that handle complexity more gracefully.

Some people will say that having less managable complexity is a "good thing" because it's feedback on your system, but sometimes the problem you are trying to solve is complex and has lots of edge cases.

This case also warrants a reference to the Blub Paradox http://c2.com/cgi/wiki?BlubParadox as a cautionary tale against limiting power in the rspec library :-)

I hope a solution like LetOverride gets added to rspec, because it would be a stretch to have an additional dependent gem for such a small piece of code. I'd rather not be on the hook to maintain it either, but I will if I have to.

let_override is also a different method name from let, so it wont change the existing functionality of let. It's simply an additional function that solves a use case and allows more (IMO better) possibilities for using rspec.

+1 for all of this. @Peeja's example using at_css is something I come up against all the time.

In specifying the behavior of a complicated object, or an object whose methods may return complicated values, it's handy to be able to "forget" about the higher-level context within a spec. When I say describe "#some_method" do, I'm saying "ok, we're going to talk about this method for a bit. I'd like subject to be able to reflect that, without a million named lets getting in the way.

Sure, I could do:

describe Bar do
  let(:instance) { Bar.new }
  super { instance }

  # some it, its, specify calls

  describe "#bar" do
    let(:bar) { instance.bar }
    super    { bar }

    # more it, its, specify calls
  end
end

But a "chained" subject (parent_subject? outer_subject?) would save me many lines over a large project, and makes a lot of stuff clearer. Telling the reader a name for something they really don't need to remember is just confusing them. With this pattern, they need only remember "ok, implicit assertions within a block are about that block's subject".

I guess I'll switch my projects to use @Peeja's patterns (right now I had the let and then just used specify { name.should something }. I was just missing the link to subject { name }; it { should something }.

I just merged into master my changes to enable use of super from let and subject declarations to handle cases like these. If anyone wants to play with it before it's out in an official release, it'd be cool to get feedback about it, and make sure it's working as expected.

Nice. I found this topic in 2019. :+1: Still working for RSpec 3.8.

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

Was this page helpful?
0 / 5 - 0 ratings