Permalink
Browse files

Merge pull request #757 from rspec/let_subject_super

Fix `let` and `subject` declarations so they can use super
  • Loading branch information...
2 parents cc2e56f + cc46b55 commit 93a5bf67da29754c0325e3c1c5d45fb143bc8b04 @myronmarston myronmarston committed Dec 31, 2012
Showing with 196 additions and 54 deletions.
  1. +3 −0 Changelog.md
  2. +61 −1 lib/rspec/core/let.rb
  3. +7 −28 lib/rspec/core/subject.rb
  4. +27 −0 spec/rspec/core/let_spec.rb
  5. +98 −25 spec/rspec/core/subject_spec.rb
View
@@ -9,6 +9,9 @@ Enhancements
* Add `subject!` that is the analog to `let!`. It defines an
explicit subject and sets a `before` hook that will invoke
the subject (Zubin Henner).
+* Fix `let` and `subject` declaration so that `super`
+ and `return` can be used in them, just like in a normal
+ method. (Myron Marston)
Bug fixes
View
@@ -2,6 +2,60 @@ module RSpec
module Core
module Let
+ # @api private
+ #
+ # Gets the LetDefinitions module. The module is mixed into
+ # the example group and is used to hold all let definitions.
+ # This is done so that the block passed to `let` can be
+ # forwarded directly on to `define_method`, so that all method
+ # constructs (including `super` and `return`) can be used in
+ # a `let` block.
+ #
+ # The memoization is provided by a method definition on the
+ # example group that supers to the LetDefinitions definition
+ # in order to get the value to memoize.
+ def self.module_for(example_group)
+ get_constant_or_yield(example_group, :LetDefinitions) do
+ # Expose `define_method` as a public method, so we can
+ # easily use it below.
+ mod = Module.new { public_class_method :define_method }
+ example_group.__send__(:include, mod)
+ example_group.const_set(:LetDefinitions, mod)
+ mod
+ end
+ end
+
+ if Module.method(:const_defined?).arity == 1 # for 1.8
+ # @api private
+ #
+ # Gets the named constant or yields.
+ # On 1.8, const_defined? / const_get do not take into
+ # account the inheritance hierarchy.
+ def self.get_constant_or_yield(example_group, name)
+ if example_group.const_defined?(name)
+ example_group.const_get(name)
+ else
+ yield
+ end
+ end
+ else
+ # @api private
+ #
+ # Gets the named constant or yields.
+ # On 1.9, const_defined? / const_get take into account the
+ # the inheritance by default, and accept an argument to
+ # disable this behavior. It's important that we don't
+ # consider inheritance here; each example group level that
+ # uses a `let` should get its own `LetDefinitions` module.
+ def self.get_constant_or_yield(example_group, name)
+ if example_group.const_defined?(name, (check_ancestors = false))
+ example_group.const_get(name, (check_ancestors = false))
+ else
+ yield
+ end
+ end
+ end
+
module ExampleGroupMethods
# Generates a method whose return value is memoized after the first
# call. Useful for reducing duplication between examples that assign
@@ -29,8 +83,14 @@ module ExampleGroupMethods
# end
# end
def let(name, &block)
+ # We have to pass the block directly to `define_method` to
+ # allow it to use method constructs like `super` and `return`.
+ ::RSpec::Core::Let.module_for(self).define_method(name, &block)
+
+ # Apply the memoization. The method has been defined in an ancestor
+ # module so we can use `super` here to get the value.
define_method(name) do
- __memoized.fetch(name) {|k| __memoized[k] = instance_eval(&block) }
+ __memoized.fetch(name) { |k| __memoized[k] = super() }
end
end
@@ -42,11 +42,10 @@ module ExampleMethods
# @see ExampleGroupMethods#subject
# @see #should
def subject
- if defined?(@original_subject)
- @original_subject
- else
- @original_subject = instance_eval(&self.class.subject)
- end
+ # This logic defines an implicit subject.
+ # Explicit `subject` declarations re-define this method.
+ described = described_class || self.class.description
+ Class === described ? described.new : described
end
# When `should` is called with no explicit receiver, the call is
@@ -193,12 +192,8 @@ def its(attribute, &block)
# @see ExampleMethods#subject
# @see ExampleMethods#should
def subject(name=nil, &block)
- if name
- let(name, &block)
- subject { send name }
- else
- block ? @explicit_subject_block = block : explicit_subject || implicit_subject
- end
+ let(:subject, &block)
+ alias_method name, :subject if name
end
# Just like `subject`, except the block is invoked by an implicit `before`
@@ -258,24 +253,8 @@ def subject!(name=nil, &block)
subject(name, &block)
before { __send__(:subject) }
end
-
- attr_reader :explicit_subject_block
-
- private
-
- def explicit_subject
- group = self
- while group.respond_to?(:explicit_subject_block)
- return group.explicit_subject_block if group.explicit_subject_block
- group = group.superclass
- end
- end
-
- def implicit_subject
- described = described_class || description
- Class === described ? proc { described.new } : proc { described }
- end
end
end
end
end
+
@@ -33,6 +33,33 @@ def count
@nil_value_count.should eq(1)
end
+
+ let(:a_value) { "a string" }
+
+ 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
+
+ context 'when the declaration uses `return`' do
+ let(:value) do
+ return :early_exit if @early_exit
+ :late_exit
+ end
+
+ it 'can exit the let declaration early' do
+ @early_exit = true
+ expect(value).to eq(:early_exit)
+ end
+
+ it 'can get past a conditional `return` statement' do
+ @early_exit = false
+ expect(value).to eq(:late_exit)
+ end
+ end
end
describe "#let!" do
@@ -5,31 +5,55 @@ module RSpec::Core
describe Subject do
before(:each) { RSpec.configuration.configure_expectation_framework }
+ def subject_value_for(describe_arg, &block)
+ group = ExampleGroup.describe(describe_arg, &block)
+ subject_value = nil
+ group.example { subject_value = subject }
+ group.run
+ subject_value
+ end
+
describe "implicit subject" do
describe "with a class" do
it "returns an instance of the class" do
- ExampleGroup.describe(Array).subject.call.should eq([])
+ expect(subject_value_for(Array)).to eq([])
end
end
describe "with a Module" do
it "returns the Module" do
- ExampleGroup.describe(Enumerable).subject.call.should eq(Enumerable)
+ expect(subject_value_for(Enumerable)).to eq(Enumerable)
end
end
describe "with a string" do
- it "return the string" do
- ExampleGroup.describe("Foo").subject.call.should eq("Foo")
+ it "returns the string" do
+ expect(subject_value_for("Foo")).to eq("Foo")
end
end
describe "with a number" do
it "returns the number" do
- ExampleGroup.describe(15).subject.call.should eq(15)
+ expect(subject_value_for(15)).to eq(15)
end
end
+ it "can be overriden and super'd to from a nested group" do
+ outer_subject_value = inner_subject_value = nil
+
+ ExampleGroup.describe(Array) do
+ subject { super() << :parent_group }
+ example { outer_subject_value = subject }
+
+ context "nested" do
+ subject { super() << :child_group }
+ example { inner_subject_value = subject }
+ end
+ end.run
+
+ expect(outer_subject_value).to eq([:parent_group])
+ expect(inner_subject_value).to eq([:parent_group, :child_group])
+ end
end
describe "explicit subject" do
@@ -54,53 +78,102 @@ module RSpec::Core
describe "defined in a top level group" do
it "replaces the implicit subject in that group" do
- group = ExampleGroup.describe(Array)
- group.subject { [1,2,3] }
- group.subject.call.should eq([1,2,3])
+ subject_value = subject_value_for(Array) do
+ subject { [1, 2, 3] }
+ end
+ expect(subject_value).to eq([1, 2, 3])
end
end
describe "defined in a top level group" do
let(:group) do
ExampleGroup.describe do
- subject{ [4,5,6] }
+ subject{ [4, 5, 6] }
end
end
it "is available in a nested group (subclass)" do
- nested_group = group.describe("I'm nested!") { }
- nested_group.subject.call.should eq([4,5,6])
+ subject_value = nil
+ group.describe("I'm nested!") do
+ example { subject_value = subject }
+ end.run
+
+ expect(subject_value).to eq([4, 5, 6])
end
it "is available in a doubly nested group (subclass)" do
- nested_group = group.describe("Nesting level 1") { }
- doubly_nested_group = nested_group.describe("Nesting level 2") { }
- doubly_nested_group.subject.call.should eq([4,5,6])
+ subject_value = nil
+ group.describe("Nesting level 1") do
+ describe("Nesting level 2") do
+ example { subject_value = subject }
+ end
+ end.run
+
+ expect(subject_value).to eq([4, 5, 6])
+ end
+
+ it "can be overriden and super'd to from a nested group" do
+ subject_value = nil
+ group.describe("Nested") do
+ subject { super() + [:override] }
+ example { subject_value = subject }
+ end.run
+
+ expect(subject_value).to eq([4, 5, 6, :override])
end
end
describe "with a name" do
it "defines a method that returns the memoized subject" do
- group = ExampleGroup.describe do
- subject(:list) { [1,2,3] }
+ list_value_1 = list_value_2 = subject_value_1 = subject_value_2 = nil
+
+ ExampleGroup.describe do
+ subject(:list) { [1, 2, 3] }
example do
- list.should equal(list)
- subject.should equal(subject)
- subject.should equal(list)
+ list_value_1 = list
+ list_value_2 = list
+ subject_value_1 = subject
+ subject_value_2 = subject
end
- end
- group.run.should be_true
+ end.run
+
+ expect(list_value_1).to eq([1, 2, 3])
+ expect(list_value_1).to equal(list_value_2)
+
+ expect(subject_value_1).to equal(subject_value_2)
+ expect(subject_value_1).to equal(list_value_1)
end
it "is referred from inside subject by the name" do
- group = ExampleGroup.describe do
- subject(:list) { [1,2,3] }
+ inner_subject_value = nil
+
+ ExampleGroup.describe do
+ subject(:list) { [1, 2, 3] }
describe 'first' do
subject(:first_element) { list.first }
- it { should eq(1) }
+ example { inner_subject_value = subject }
end
+ end.run
+
+ expect(inner_subject_value).to eq(1)
+ end
+
+ context 'when `super` is used' do
+ it "delegates to the parent context's `subject`, not the named mehtod" do
+ inner_subject_value = nil
+
+ ExampleGroup.describe do
+ let(:list) { ["a", "b", "c"] }
+ subject { [1, 2, 3] }
+
+ describe 'first' do
+ subject(:list) { super().first(2) }
+ example { inner_subject_value = subject }
+ end
+ end.run
+
+ expect(inner_subject_value).to eq([1, 2])
end
- group.run.should be_true
end
end
end

0 comments on commit 93a5bf6

Please sign in to comment.