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:
I'm happy to write the implementation.
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 let
s and subject
s. 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 ExampleGroup
s being classes which inherit from each other, subjects and let blocks could be define_method
ed 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 let
s.
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.
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
Most helpful comment
Nice. I found this topic in 2019. :+1: Still working for RSpec 3.8.