Skip to content
This repository

Implicit subject should be memoized with let #768

Merged
merged 2 commits into from over 1 year ago

3 participants

Olek Janiszewski Myron Marston David Chelimsky
Olek Janiszewski

This is a partial solution to #766, the only thing left is the problem with its.

I'm open to feedback and suggestions on improving this solution.

lib/rspec/core/example_group.rb
... ...
@@ -13,6 +13,8 @@ module Core
13 13
     # which serves as a wrapper for an instance of the ExampleGroup in which it
14 14
     # is declared.
15 15
     class ExampleGroup
  16
+      include Let # Subject::ExampleGroupMethods depends on this module, so include it first
  17
+
3
David Chelimsky Owner

That's fine if there's an ordering dependency, but this can just go right before Subject::ExampleMethods within the group of include statements rather than making it the very first thing and separating it from the other includes.

Olek Janiszewski
exviva added a note

Unfortunately it depends on Subject::ExampleGroupMethods, not Subject::ExampleMethods, which is in the extended modules, before all the includes.

David Chelimsky Owner

Got it - misread the comment :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/rspec/core/subject.rb
((5 lines not shown))
193 180
         # @see ExampleMethods#should
194 181
         def subject(name=nil, &block)
195 182
           let(:subject, &block)
196 183
           alias_method name, :subject if name
197 184
         end
198 185
 
  186
+        def self.extended(base)
  187
+          # This logic defines an implicit subject.
6
David Chelimsky Owner

How about a method named implicit_subject instead of a comment?

Olek Janiszewski
exviva added a note

:+1: I'll wait for you to finish your review and mass-amend the PR.

David Chelimsky Owner

I'm done, but @myronmarston and @alindeman might have different comments.

Myron Marston Owner

I actually prefer this to defining a method named implicit_subject. Each method that gets mixed in to example groups is another potential naming collision waiting to happen, since these methods are added to the same namespace where users define methods. Ideally, RSpec will "reserve" as few names as possible.

David Chelimsky Owner

@myronmarston your reasoning makes sense to me.

Olek Janiszewski
exviva added a note

That was my intention as well (in a commit regarding its I removed all other private methods). I just moved the comment from the original implementation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
spec/rspec/core/subject_spec.rb
... ...
@@ -338,4 +338,34 @@ def false_if_first_time
338 338
       end
339 339
     end
340 340
   end
  341
+
  342
+  describe 'using subject in before and let blocks' do
  343
+    shared_examples_for 'a subject' do
  344
+      let(:subject_id_in_let) { subject.object_id }
  345
+      subject_id_in_before = nil
2
David Chelimsky Owner

Having this assignment "unwrapped" feels a little weird to me. The reason is that it will be evaluated when the shared_examples block is evaluated rather than within the scope of the example.

How about using an instance variable in the before hook? e.g.

before { @subject_id_in_before = subject.object_id }

it 'should be memoized' do
  expect(subject_id_in_let).to eq(@subject_id_in_before)
end
Myron Marston Owner

This looks a bit like a technique I blogged about :). I'm comfortable with using a pre-declared local here, but others might find it confusing...and the sorts of reasons I would usually favor this technique over ivars (i.e. limiting the number of variables that "leak" into an including context) don't really apply here. I think the instance variable version recommended by @dchelimsky is more easily readable by more people.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/rspec/core/subject.rb
((21 lines not shown))
  21
+    #       # ^ ^ explicit reference to subject not recommended
  22
+    #     end
  23
+    #   end
  24
+    #
  25
+    #   # implicit subject => { Person.new }
  26
+    #   describe Person do
  27
+    #     it "should be eligible to vote" do
  28
+    #       subject.should be_eligible_to_vote
  29
+    #       # ^ ^ explicit reference to subject not recommended
  30
+    #     end
  31
+    #   end
  32
+    #
  33
+    #   # one-liner syntax - should is invoked on subject
  34
+    #   describe Person do
  35
+    #     it { should be_eligible_to_vote }
  36
+    #   end
8
Myron Marston Owner

I can see why moving the YARD comments are necessary (since we no longer have def subject), but I'm a bit concerned about how this changes the docs. Currently the docs show up here. With this change I assume they'll show up in the overview section of the module? Is there a way with yard to force-add docs for subject even though we don't have def subject?

Olek Janiszewski
exviva added a note

I don't know YARD too well, but I'd doubt it. Maybe this section should be moved to ExampleGroupMethods#subject?

Myron Marston Owner

Actually, I think we can leave a normal def subject with these comments in place so the docs make sense. That version of the method will never be called (as we re-define it with your extended hook below) but it will make the docs more clear, I think.

@dchelimsky -- can you think of a better way to handle the doc needs here?

Olek Janiszewski
exviva added a note

@myronmarston adding an empty ExampleMethods#subject is a bit unfortunate - since this module is included after ExampleGroupMethods gets extended, this method becomes the implicit subject (and first super() in the chain). I'd need to re-organise the includes and extends again. Do you think it's worth it?

Myron Marston Owner

I think so. Having easily findable docs for what subject does when called from an example seems important.

That said, given these ordering dependencies, it makes me think we should make the module here mange it (e.g. via an included hook), rather than forcing ExampleGroup to include/extend things in a particular order. Also, given the similarity between subject and let, and the fact that one now depends on the other...I wonder if we should combine these into one module in one file? That'd have a nice side benefit of loading one fewer file at runtime when rspec is loading, leading to ever-so-slightly-faster (but barely noticeable if at all) runtimes.

Olek Janiszewski
exviva added a note

Combining these two modules into one seemed to be the most natural for me too. Any suggestions for the name? How about RSpec::Core::Declarative?

This may be getting a bit ahead of myself, but do you have any policy regarding deprecating constants (modules)?

Myron Marston Owner

Declarative is a good suggestion. Another idea: RSpec::Core::MemoizedHelpers -- since both let and subject define memoized helper methods.

This may be getting a bit ahead of myself, but do you have any policy regarding deprecating constants (modules)?

We try to follow SemVer but there are gray areas for cases like these. In a case like an error class that I expect someone to explicitly reference (e.g. to rescue that kind of error) I'd definitely want to properly deprecate it using a technique I blogged about. That said, I don't consider the constant names for the Let and Subject modules to be part of the public API; it's simply the internal code organization we've chosen to use within rspec-core. The public API are the let and subject methods made available in example groups for end users, and the modules they come from shouldn't matter. I can't think of a case where a user would be explicitly referencing one of these modules.

Olek Janiszewski
exviva added a note

Another idea: RSpec::Core::MemoizedHelpers -- since both let and subject define memoized helper methods.

The full API this module adds, is:

#subject
#should
#should_not
.let
.let!
.subject
.subject!
.its

So it's not only about memoization...how about ShorthandSyntax? Nah, doesn't sound good, too :).

Ok, I'll call it MemoizedHelpers for now.

I don't consider the constant names for the Let and Subject modules to be part of the public API

Cool :).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Myron Marston
Owner

This is fantastic, @exviva! Are you planning to tackle the its issue as well?

Olek Janiszewski

@myronmarston I'm struggling with the its issue, but no luck (yet). You can have a look at my efforts. Since its is broken also in v2.12.2, I'd say these are 2 separate issues, so how about we merge this one in first and I'd work on its in parallel?

Myron Marston
Owner

@myronmarston I'm struggling with the its issue, but no luck (yet). You can have a look at my efforts. Since its is broken also in v2.12.2, I'd say these are 2 separate issues, so how about we merge this one in first and I'd work on its in parallel?

Thanks, I'll take a look. I don't have the time right now but hope to get to it this weekend.

And yes, I consider these 2 separate issues that can be merged separately. I'd like to figure out what we're doing about the docs at first, though.

Olek Janiszewski

@myronmarston I've combined the 2 modules, please have a look.

One side effect of this is that SharedContext now has the subject, subject! and its class methods (since they're now part of the same module as let and let!). This is undocumented and untested behaviour, and I don't think it's harmful, so I guess we could leave it like this. If you disagree, I'll extract these sets of methods into 2 separate modules.

Myron Marston myronmarston commented on the diff
lib/rspec/core/memoized_helpers.rb
((345 lines not shown))
  345
+              self.class.class_eval do
  346
+                define_method(:subject) do
  347
+                  if defined?(@_subject)
  348
+                    @_subject
  349
+                  else
  350
+                    @_subject = Array === attribute ? super()[*attribute] : _nested_attribute(super(), attribute)
  351
+                  end
  352
+                end
  353
+              end
  354
+              instance_eval(&block)
  355
+            end
  356
+          end
  357
+        end
  358
+      end
  359
+
  360
+      module LetDefinitions
2
Myron Marston Owner

I don't think we need to put these methods into this module...they can just exist as direct singleton methods on the MemoizedHelpers module. Singleton methods don't get added to the host class when the module is mixed in so there's no danger of "leaking" these methods into the user's namespace.

Plus, I think it's kinda confusing that the code below looks for a module named LetDefinition and this one is also called LetDefinitions, but the module it is looking for is not this module....it's example_group::LetDefinitions.

Olek Janiszewski
exviva added a note

Will do.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Myron Marston
Owner

One side effect of this is that SharedContext now has the subject, subject! and its class methods (since they're now part of the same module as let and let!). This is undocumented and untested behaviour, and I don't think it's harmful, so I guess we could leave it like this. If you disagree, I'll extract these sets of methods into 2 separate modules.

Actually, I noticed that SharedContext lacked these methods a while ago and thought it was a bit of a curious oversight (or bug, arguably). If the new added methods work fine with SharedContext then we should keep the, but add specs to demonstrate that they work. If they don't work, we should either find a way to make them work or remove them because it would be confusing to have them if they didn't work. If we remove them, I'm a little loath to separate this into 2 modules again, though...so I wonder if it would be simpler to use undef in the SharedContext module to remove any methods that don't work?

Alternately, shared_context provides all of the RSpec DSL methods, and the implementation is simpler than SharedContex, to boot. I've never used SharedContext and never seen it used in the wild. @dchelimsky, what do you think about the possibility of deprecating SharedContext with the plan to remove it in 3.0? Or do you think there are use cases that SharedContext meets better than shared_context? I'm not sure we need both...

Olek Janiszewski

Ok, I think I'm getting somewhere. Have a look at these examples:

describe 'Implicit subject instantiates the most top-level class used in `describe`' do
#  it { should be_an_instance_of(String) }
  its(:class) { should eq(String) }

  describe OuterClass do
    it { should be_an_instance_of(OuterClass) }
    its(:class) { should eq(OuterClass) }

    describe 'another string' do
      it { should be_an_instance_of(OuterClass) }
      its(:class) { should eq(OuterClass) }

      describe InnerClass do
        it { should be_an_instance_of(OuterClass) }
        its(:class) { should eq(OuterClass) }
      end
    end
  end
end

They all pass on v2.12.2, but only until I...uncomment the first it { should .... As soon as I uncomment this line, all but the first 2 examples fail. I think this is a bug in v2.12.2, but nobody has even used nested groups this way. Most probably people have a top-level class, and then strings, as descriptions.

My proposed solution is to mix in the implicit subject only if the described class is a class or module. Otherwise there should be no implicit subject. This way you can nest groups multiple times, mixing strings and classes/modules as descriptions, and the implicit subject will always instantiate the closest Class (or return the closest Module), which seems most intuitive.

Another idea I see is to mix in the implicit subject only in the top-level example group, but this doesn't feel right if people want to nest describe MyClass inside describe 'a bigger story'.

The problem with its (which turned out to be just a tip of the iceberg) is that the implicit subject gets implemented on each example group, so as soon as I do describe Foo; describe 'bar', 'bar' becomes the implicit subject in the inner group. Now since its uses a nested group and calls super(), they're using the implicit subject for that nested group, not the subject from the parent group.

Myron Marston myronmarston merged commit 0c70d63 into from
Myron Marston myronmarston closed this
Myron Marston
Owner

@exviva -- I started looking into the its stuff but since it needs implicit subject memoization to work correctly, it made sense to go ahead and merge what you have here. I took care of removing the unneeded LetDefinitions module, too.

Olek Janiszewski exviva deleted the branch
Myron Marston
Owner

I also cherry-picked over your its refactoring because it makes it much clearer and simpler than before.

Olek Janiszewski

Cool. I finally understood, why the examples with its fail:

describe 'using its with before and let blocks' do
  subject { :symbol } # or whatever
  let(:subject_id_in_let) { subject.object_id }
  before { @subject_id_in_before = subject.object_id }

  its(:object_id) { should eq(subject_id_in_let) }
  its(:object_id) { should eq(@subject_id_in_before) }
end

It's because when let and before are evaluated, the subject they're referring to is actually :symbol.object_id, since its overrides subject, so hey end up calling :symbol.object_id.object_id. It's as if we wrote:

describe 'using its with before and let blocks' do
  subject { :symbol }
  let(:subject_id_in_let) { subject.object_id } # easy to think it'd always return :symbol.object_id...

  describe(:object_id) do
    subject { :symbol.object_id }
    it { should eq(subject_id_in_let) } # but here it actually returns :symbol.object_id.object_id
  end
end

In other words, doh...:). Actually, I'd call it a bug, because I'd still expect subject to refer to the outer subject, not the internal its subject. But we'd then probably need a special case in the code, I doubt that it's worth it.

I'll try master on my project on Monday, and come back if I find any more issues.

I've had some weird behaviour of described_class returning different values in a nested group, when running all specs or using the line number filter to run only the innermost group, or depending on whether or not some examples are commented out, but I'm too tired now to isolate it :).

Myron Marston
Owner

Nice work getting to the bottom of this. That's very, very subtle, and is another argument for using its sparingly, if at all...we plan to move it out of rspec-core into another gem in rspec-3 to signal that it's not really core to rspec, so to speak.

I've had some weird behaviour of described_class returning different values in a nested group, when running all specs or using the line number filter to run only the innermost group, or depending on whether or not some examples are commented out, but I'm too tired now to isolate it :).

Yep, I was playing with the example you pasted above, too, and there is something weird going on. I'm going to play with it more to get to the bottom of it.

And actually, in your example above, here's how I actually think it should behave:

describe 'Implicit subject instantiates the most top-level class used in `describe`' do
#  it { should be_an_instance_of(String) }
  its(:class) { should eq(String) }

  describe OuterClass do
    it { should be_an_instance_of(OuterClass) }
    its(:class) { should eq(OuterClass) }

    describe 'another string' do
      it { should be_an_instance_of(OuterClass) }
      its(:class) { should eq(OuterClass) }

      describe InnerClass do
        it { should be_an_instance_of(InnerClass) }
        its(:class) { should eq(InnerClass) }
      end
    end
  end
end

In other words, I think the implicit subject should be an instance of the inner-most described class, or if, there is not described class, the outermost described thing.

@dchelimsky -- any thoughts on what the correct thing is when nesting a describe SomeClass inside another?

Myron Marston
Owner

@exviva -- I just pushed a fix to described_class in 538285b that was the root of the weirdness you mentioned above. Want to give that a try to see if it solves your problems?

Olek Janiszewski

@myronmarston nice work with described_class!

I've just used 538285b in my project, and unfortunately it still has a regression in its. Here's the failing example (passing on v2.12.2):

describe Coupon do
  describe 'delegating name to source' do
    let(:name) { 'Best program ever!' }
    before { subject.source = LoyaltyProgram.new(name: name) }

    its(:name) { should eq(name) }
  end
end

Failure is RuntimeError: Coupon#name delegated to source.name, but source is nil.

I'll investigate it a bit today.

Olek Janiszewski

@myronmarston here's a solution I came up with to keep its behaviour backward compatible. I've used my project's specs to check if it's working.

If you like it, I'll cover it with proper specs and docs.

Myron Marston
Owner

@exviva -- it's hard to say whether or not I like that solution because it's not clear to me what the bug is. Can you come up with an rspec-core commit that adds a failing example for your use case?

Olek Janiszewski

Ah, sorry. The bug is that its changes subject, so before and let blocks refer to the wrong subject. This is passing on v2.12.2, but fails on master:

MyClass = Struct.new(:some_attr)

describe MyClass do
  before { subject.some_attr = :foo } # [1]
  its(:some_attr) { should eq(:foo) }
end

On line [1], subject used to refer to MyClass.new (or whatever the subject for that level was), now it refers to MyClass.new.some_attr, because its overrides subject.

I'll write a failing example in a moment.

Myron Marston
Owner

Ah, I see what the problem is now...in 2.12.2 (and before), its defined subject from within the example--which means it waited until the example ran to override it--which means that before hooks like in this case would run with the old definition of subject. It seemed very odd to me, and with your refactoring to its, we moved the redefinition of subject into the example group defined by its--which is more natural (and less odd), but breaks this behavior. I think we should just move it back into the example, as that doesn't make us introduce a new method like subject_for_receiverless_should.

Olek Janiszewski

@myronmarston I don't think this will solve the problem, at least not for let - if a subject-accessing let block is lazily evaluated for the first time from the its block, it'll have access to the wrong subject.

We can either have Example#should and Exampe#should_not call a different method (like my patch showed), or we could include/extend a module which overrides these two methods. WDYT?

Myron Marston
Owner

I don't think this will solve the problem, at least not for let - if a subject-accessing let block is lazily evaluated for the first time from the its block, it'll have access to the wrong subject.

Hmm, you're right. That said, I believe that has been the behavior of let in a its block for many, many rspec releases--so I wouldn't consider this a regression.

We can either have Example#should and Exampe#should_not call a different method (like my patch showed), or we could include/extend a module which overrides these two methods. WDYT?

The latter approach appeals to me because I'd prefer not to introduce another method (for reasons we've discussed previously). However, I'm not convinced that it'll solve the problem; changing what should and should_not delegate to won't affect a let declaration that references subject right?

Olek Janiszewski

Including a module does the trick: https://github.com/exviva/rspec-core/compare/its_subject.

But there is no way of solving this without adding a new method to the example (with def or let). After all, if let refers to subject, and its(:foo) { should ..., calls subject too, they'll return the same object, which we don't want.

And since I'd expect let { subject ... } to access the outer subject, the only way to fix this is to make #should inside its not access the same subject. Phew, this is hard to explain :).

Have a look at my branch, let me know if it makes sense, if yes, I'll add some more docs and open a PR.

Myron Marston
Owner

I like that solution a lot. My one suggestion is to see if we can make the specs for that more clear...they're a bit convoluted. Please open a PR for it!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
3  lib/rspec/core.rb
@@ -23,8 +23,7 @@
23 23
 
24 24
 require_rspec['core/metadata_hash_builder']
25 25
 require_rspec['core/hooks']
26  
-require_rspec['core/subject']
27  
-require_rspec['core/let']
  26
+require_rspec['core/memoized_helpers']
28 27
 require_rspec['core/metadata']
29 28
 require_rspec['core/pending']
30 29
 require_rspec['core/formatters']
4  lib/rspec/core/example_group.rb
@@ -15,13 +15,11 @@ module Core
15 15
     class ExampleGroup
16 16
       extend  MetadataHashBuilder::WithDeprecationWarning
17 17
       extend  Extensions::ModuleEvalWithArgs
18  
-      extend  Subject::ExampleGroupMethods
19 18
       extend  Hooks
20 19
 
  20
+      include MemoizedHelpers
21 21
       include Extensions::InstanceEvalWithArgs
22  
-      include Subject::ExampleMethods
23 22
       include Pending
24  
-      include Let
25 23
       include SharedExampleGroup
26 24
 
27 25
       # @private
166  lib/rspec/core/let.rb
... ...
@@ -1,166 +0,0 @@
1  
-module RSpec
2  
-  module Core
3  
-    module Let
4  
-
5  
-      # @api private
6  
-      #
7  
-      # Gets the LetDefinitions module. The module is mixed into
8  
-      # the example group and is used to hold all let definitions.
9  
-      # This is done so that the block passed to `let` can be
10  
-      # forwarded directly on to `define_method`, so that all method
11  
-      # constructs (including `super` and `return`) can be used in
12  
-      # a `let` block.
13  
-      #
14  
-      # The memoization is provided by a method definition on the
15  
-      # example group that supers to the LetDefinitions definition
16  
-      # in order to get the value to memoize.
17  
-      def self.module_for(example_group)
18  
-        get_constant_or_yield(example_group, :LetDefinitions) do
19  
-          # Expose `define_method` as a public method, so we can
20  
-          # easily use it below.
21  
-          mod = Module.new { public_class_method :define_method }
22  
-          example_group.__send__(:include, mod)
23  
-          example_group.const_set(:LetDefinitions, mod)
24  
-          mod
25  
-        end
26  
-      end
27  
-
28  
-      if Module.method(:const_defined?).arity == 1 # for 1.8
29  
-        # @api private
30  
-        #
31  
-        # Gets the named constant or yields.
32  
-        # On 1.8, const_defined? / const_get do not take into
33  
-        # account the inheritance hierarchy.
34  
-        def self.get_constant_or_yield(example_group, name)
35  
-          if example_group.const_defined?(name)
36  
-            example_group.const_get(name)
37  
-          else
38  
-            yield
39  
-          end
40  
-        end
41  
-      else
42  
-        # @api private
43  
-        #
44  
-        # Gets the named constant or yields.
45  
-        # On 1.9, const_defined? / const_get take into account the
46  
-        # the inheritance by default, and accept an argument to
47  
-        # disable this behavior. It's important that we don't
48  
-        # consider inheritance here; each example group level that
49  
-        # uses a `let` should get its own `LetDefinitions` module.
50  
-        def self.get_constant_or_yield(example_group, name)
51  
-          if example_group.const_defined?(name, (check_ancestors = false))
52  
-            example_group.const_get(name, check_ancestors)
53  
-          else
54  
-            yield
55  
-          end
56  
-        end
57  
-      end
58  
-
59  
-      module ExampleGroupMethods
60  
-        # Generates a method whose return value is memoized after the first
61  
-        # call. Useful for reducing duplication between examples that assign
62  
-        # values to the same local variable.
63  
-        #
64  
-        # @note `let` _can_ enhance readability when used sparingly (1,2, or
65  
-        #   maybe 3 declarations) in any given example group, but that can
66  
-        #   quickly degrade with overuse. YMMV.
67  
-        #
68  
-        # @note `let` uses an `||=` conditional that has the potential to
69  
-        #   behave in surprising ways in examples that spawn separate threads,
70  
-        #   though we have yet to see this in practice. You've been warned.
71  
-        #
72  
-        # @example
73  
-        #
74  
-        #   describe Thing do
75  
-        #     let(:thing) { Thing.new }
76  
-        #
77  
-        #     it "does something" do
78  
-        #       # first invocation, executes block, memoizes and returns result
79  
-        #       thing.do_something
80  
-        #
81  
-        #       # second invocation, returns the memoized value
82  
-        #       thing.should be_something
83  
-        #     end
84  
-        #   end
85  
-        def let(name, &block)
86  
-          # We have to pass the block directly to `define_method` to
87  
-          # allow it to use method constructs like `super` and `return`.
88  
-          ::RSpec::Core::Let.module_for(self).define_method(name, &block)
89  
-
90  
-          # Apply the memoization. The method has been defined in an ancestor
91  
-          # module so we can use `super` here to get the value.
92  
-          define_method(name) do
93  
-            __memoized.fetch(name) { |k| __memoized[k] = super() }
94  
-          end
95  
-        end
96  
-
97  
-        # Just like `let`, except the block is invoked by an implicit `before`
98  
-        # hook. This serves a dual purpose of setting up state and providing a
99  
-        # memoized reference to that state.
100  
-        #
101  
-        # @example
102  
-        #
103  
-        #   class Thing
104  
-        #     def self.count
105  
-        #       @count ||= 0
106  
-        #     end
107  
-        #
108  
-        #     def self.count=(val)
109  
-        #       @count += val
110  
-        #     end
111  
-        #
112  
-        #     def self.reset_count
113  
-        #       @count = 0
114  
-        #     end
115  
-        #
116  
-        #     def initialize
117  
-        #       self.class.count += 1
118  
-        #     end
119  
-        #   end
120  
-        #
121  
-        #   describe Thing do
122  
-        #     after(:each) { Thing.reset_count }
123  
-        #
124  
-        #     context "using let" do
125  
-        #       let(:thing) { Thing.new }
126  
-        #
127  
-        #       it "is not invoked implicitly" do
128  
-        #         Thing.count.should eq(0)
129  
-        #       end
130  
-        #
131  
-        #       it "can be invoked explicitly" do
132  
-        #         thing
133  
-        #         Thing.count.should eq(1)
134  
-        #       end
135  
-        #     end
136  
-        #
137  
-        #     context "using let!" do
138  
-        #       let!(:thing) { Thing.new }
139  
-        #
140  
-        #       it "is invoked implicitly" do
141  
-        #         Thing.count.should eq(1)
142  
-        #       end
143  
-        #
144  
-        #       it "returns memoized version on first invocation" do
145  
-        #         thing
146  
-        #         Thing.count.should eq(1)
147  
-        #       end
148  
-        #     end
149  
-        #   end
150  
-        def let!(name, &block)
151  
-          let(name, &block)
152  
-          before { __send__(name) }
153  
-        end
154  
-      end
155  
-
156  
-      # @private
157  
-      def __memoized
158  
-        @__memoized ||= {}
159  
-      end
160  
-
161  
-      def self.included(mod)
162  
-        mod.extend ExampleGroupMethods
163  
-      end
164  
-    end
165  
-  end
166  
-end
418  lib/rspec/core/memoized_helpers.rb
... ...
@@ -0,0 +1,418 @@
  1
+module RSpec
  2
+  module Core
  3
+    module MemoizedHelpers
  4
+      # @note `subject` was contributed by Joe Ferris to support the one-liner
  5
+      #   syntax embraced by shoulda matchers:
  6
+      #
  7
+      #       describe Widget do
  8
+      #         it { should validate_presence_of(:name) }
  9
+      #       end
  10
+      #
  11
+      #   While the examples below demonstrate how to use `subject`
  12
+      #   explicitly in examples, we recommend that you define a method with
  13
+      #   an intention revealing name instead.
  14
+      #
  15
+      # @example
  16
+      #
  17
+      #   # explicit declaration of subject
  18
+      #   describe Person do
  19
+      #     subject { Person.new(:birthdate => 19.years.ago) }
  20
+      #     it "should be eligible to vote" do
  21
+      #       subject.should be_eligible_to_vote
  22
+      #       # ^ ^ explicit reference to subject not recommended
  23
+      #     end
  24
+      #   end
  25
+      #
  26
+      #   # implicit subject => { Person.new }
  27
+      #   describe Person do
  28
+      #     it "should be eligible to vote" do
  29
+      #       subject.should be_eligible_to_vote
  30
+      #       # ^ ^ explicit reference to subject not recommended
  31
+      #     end
  32
+      #   end
  33
+      #
  34
+      #   # one-liner syntax - should is invoked on subject
  35
+      #   describe Person do
  36
+      #     it { should be_eligible_to_vote }
  37
+      #   end
  38
+      #
  39
+      # @see #should
  40
+      def subject
  41
+        raise NotImplementedError, 'This definition is here for documentation purposes only'
  42
+          ' - it is overriden anyway below when this module gets included.'
  43
+      end
  44
+
  45
+      # When `should` is called with no explicit receiver, the call is
  46
+      # delegated to the object returned by `subject`. Combined with an
  47
+      # implicit subject this supports very concise expressions.
  48
+      #
  49
+      # @example
  50
+      #
  51
+      #   describe Person do
  52
+      #     it { should be_eligible_to_vote }
  53
+      #   end
  54
+      #
  55
+      # @see #subject
  56
+      def should(matcher=nil, message=nil)
  57
+        RSpec::Expectations::PositiveExpectationHandler.handle_matcher(subject, matcher, message)
  58
+      end
  59
+
  60
+      # Just like `should`, `should_not` delegates to the subject (implicit or
  61
+      # explicit) of the example group.
  62
+      #
  63
+      # @example
  64
+      #
  65
+      #   describe Person do
  66
+      #     it { should_not be_eligible_to_vote }
  67
+      #   end
  68
+      #
  69
+      # @see #subject
  70
+      def should_not(matcher=nil, message=nil)
  71
+        RSpec::Expectations::NegativeExpectationHandler.handle_matcher(subject, matcher, message)
  72
+      end
  73
+
  74
+      private
  75
+
  76
+      def _attribute_chain(attribute)
  77
+        attribute.to_s.split('.')
  78
+      end
  79
+
  80
+      def _nested_attribute(subject, attribute)
  81
+        _attribute_chain(attribute).inject(subject) do |inner_subject, attr|
  82
+          inner_subject.send(attr)
  83
+        end
  84
+      end
  85
+
  86
+      # @private
  87
+      def __memoized
  88
+        @__memoized ||= {}
  89
+      end
  90
+
  91
+      def self.included(mod)
  92
+        mod.extend(ClassMethods)
  93
+
  94
+        # This logic defines an implicit subject
  95
+        mod.subject do
  96
+          described = described_class || self.class.description
  97
+          Class === described ? described.new : described
  98
+        end
  99
+      end
  100
+
  101
+      module ClassMethods
  102
+        # Generates a method whose return value is memoized after the first
  103
+        # call. Useful for reducing duplication between examples that assign
  104
+        # values to the same local variable.
  105
+        #
  106
+        # @note `let` _can_ enhance readability when used sparingly (1,2, or
  107
+        #   maybe 3 declarations) in any given example group, but that can
  108
+        #   quickly degrade with overuse. YMMV.
  109
+        #
  110
+        # @note `let` uses an `||=` conditional that has the potential to
  111
+        #   behave in surprising ways in examples that spawn separate threads,
  112
+        #   though we have yet to see this in practice. You've been warned.
  113
+        #
  114
+        # @example
  115
+        #
  116
+        #   describe Thing do
  117
+        #     let(:thing) { Thing.new }
  118
+        #
  119
+        #     it "does something" do
  120
+        #       # first invocation, executes block, memoizes and returns result
  121
+        #       thing.do_something
  122
+        #
  123
+        #       # second invocation, returns the memoized value
  124
+        #       thing.should be_something
  125
+        #     end
  126
+        #   end
  127
+        def let(name, &block)
  128
+          # We have to pass the block directly to `define_method` to
  129
+          # allow it to use method constructs like `super` and `return`.
  130
+          ::RSpec::Core::MemoizedHelpers::LetDefinitions.module_for(self).define_method(name, &block)
  131
+
  132
+          # Apply the memoization. The method has been defined in an ancestor
  133
+          # module so we can use `super` here to get the value.
  134
+          define_method(name) do
  135
+            __memoized.fetch(name) { |k| __memoized[k] = super() }
  136
+          end
  137
+        end
  138
+
  139
+        # Just like `let`, except the block is invoked by an implicit `before`
  140
+        # hook. This serves a dual purpose of setting up state and providing a
  141
+        # memoized reference to that state.
  142
+        #
  143
+        # @example
  144
+        #
  145
+        #   class Thing
  146
+        #     def self.count
  147
+        #       @count ||= 0
  148
+        #     end
  149
+        #
  150
+        #     def self.count=(val)
  151
+        #       @count += val
  152
+        #     end
  153
+        #
  154
+        #     def self.reset_count
  155
+        #       @count = 0
  156
+        #     end
  157
+        #
  158
+        #     def initialize
  159
+        #       self.class.count += 1
  160
+        #     end
  161
+        #   end
  162
+        #
  163
+        #   describe Thing do
  164
+        #     after(:each) { Thing.reset_count }
  165
+        #
  166
+        #     context "using let" do
  167
+        #       let(:thing) { Thing.new }
  168
+        #
  169
+        #       it "is not invoked implicitly" do
  170
+        #         Thing.count.should eq(0)
  171
+        #       end
  172
+        #
  173
+        #       it "can be invoked explicitly" do
  174
+        #         thing
  175
+        #         Thing.count.should eq(1)
  176
+        #       end
  177
+        #     end
  178
+        #
  179
+        #     context "using let!" do
  180
+        #       let!(:thing) { Thing.new }
  181
+        #
  182
+        #       it "is invoked implicitly" do
  183
+        #         Thing.count.should eq(1)
  184
+        #       end
  185
+        #
  186
+        #       it "returns memoized version on first invocation" do
  187
+        #         thing
  188
+        #         Thing.count.should eq(1)
  189
+        #       end
  190
+        #     end
  191
+        #   end
  192
+        def let!(name, &block)
  193
+          let(name, &block)
  194
+          before { __send__(name) }
  195
+        end
  196
+
  197
+        # Declares a `subject` for an example group which can then be the
  198
+        # implicit receiver (through delegation) of calls to `should`.
  199
+        #
  200
+        # Given a `name`, defines a method with that name which returns the
  201
+        # `subject`. This lets you declare the subject once and access it
  202
+        # implicitly in one-liners and explicitly using an intention revealing
  203
+        # name.
  204
+        #
  205
+        # @param [String,Symbol] name used to define an accessor with an
  206
+        #   intention revealing name
  207
+        # @param block defines the value to be returned by `subject` in examples
  208
+        #
  209
+        # @example
  210
+        #
  211
+        #   describe CheckingAccount, "with $50" do
  212
+        #     subject { CheckingAccount.new(Money.new(50, :USD)) }
  213
+        #     it { should have_a_balance_of(Money.new(50, :USD)) }
  214
+        #     it { should_not be_overdrawn }
  215
+        #   end
  216
+        #
  217
+        #   describe CheckingAccount, "with a non-zero starting balance" do
  218
+        #     subject(:account) { CheckingAccount.new(Money.new(50, :USD)) }
  219
+        #     it { should_not be_overdrawn }
  220
+        #     it "has a balance equal to the starting balance" do
  221
+        #       account.balance.should eq(Money.new(50, :USD))
  222
+        #     end
  223
+        #   end
  224
+        #
  225
+        # @see MemoizedHelpers#should
  226
+        def subject(name=nil, &block)
  227
+          let(:subject, &block)
  228
+          alias_method name, :subject if name
  229
+        end
  230
+
  231
+        # Just like `subject`, except the block is invoked by an implicit `before`
  232
+        # hook. This serves a dual purpose of setting up state and providing a
  233
+        # memoized reference to that state.
  234
+        #
  235
+        # @example
  236
+        #
  237
+        #   class Thing
  238
+        #     def self.count
  239
+        #       @count ||= 0
  240
+        #     end
  241
+        #
  242
+        #     def self.count=(val)
  243
+        #       @count += val
  244
+        #     end
  245
+        #
  246
+        #     def self.reset_count
  247
+        #       @count = 0
  248
+        #     end
  249
+        #
  250
+        #     def initialize
  251
+        #       self.class.count += 1
  252
+        #     end
  253
+        #   end
  254
+        #
  255
+        #   describe Thing do
  256
+        #     after(:each) { Thing.reset_count }
  257
+        #
  258
+        #     context "using subject" do
  259
+        #       subject { Thing.new }
  260
+        #
  261
+        #       it "is not invoked implicitly" do
  262
+        #         Thing.count.should eq(0)
  263
+        #       end
  264
+        #
  265
+        #       it "can be invoked explicitly" do
  266
+        #         subject
  267
+        #         Thing.count.should eq(1)
  268
+        #       end
  269
+        #     end
  270
+        #
  271
+        #     context "using subject!" do
  272
+        #       subject!(:thing) { Thing.new }
  273
+        #
  274
+        #       it "is invoked implicitly" do
  275
+        #         Thing.count.should eq(1)
  276
+        #       end
  277
+        #
  278
+        #       it "returns memoized version on first invocation" do
  279
+        #         subject
  280
+        #         Thing.count.should eq(1)
  281
+        #       end
  282
+        #     end
  283
+        #   end
  284
+        def subject!(name=nil, &block)
  285
+          subject(name, &block)
  286
+          before { subject }
  287
+        end
  288
+
  289
+        # Creates a nested example group named by the submitted `attribute`,
  290
+        # and then generates an example using the submitted block.
  291
+        #
  292
+        # @example
  293
+        #
  294
+        #   # This ...
  295
+        #   describe Array do
  296
+        #     its(:size) { should eq(0) }
  297
+        #   end
  298
+        #
  299
+        #   # ... generates the same runtime structure as this:
  300
+        #   describe Array do
  301
+        #     describe "size" do
  302
+        #       it "should eq(0)" do
  303
+        #         subject.size.should eq(0)
  304
+        #       end
  305
+        #     end
  306
+        #   end
  307
+        #
  308
+        # The attribute can be a `Symbol` or a `String`. Given a `String`
  309
+        # with dots, the result is as though you concatenated that `String`
  310
+        # onto the subject in an expression.
  311
+        #
  312
+        # @example
  313
+        #
  314
+        #   describe Person do
  315
+        #     subject do
  316
+        #       Person.new.tap do |person|
  317
+        #         person.phone_numbers << "555-1212"
  318
+        #       end
  319
+        #     end
  320
+        #
  321
+        #     its("phone_numbers.first") { should eq("555-1212") }
  322
+        #   end
  323
+        #
  324
+        # When the subject is a `Hash`, you can refer to the Hash keys by
  325
+        # specifying a `Symbol` or `String` in an array.
  326
+        #
  327
+        # @example
  328
+        #
  329
+        #   describe "a configuration Hash" do
  330
+        #     subject do
  331
+        #       { :max_users => 3,
  332
+        #         'admin' => :all_permissions }
  333
+        #     end
  334
+        #
  335
+        #     its([:max_users]) { should eq(3) }
  336
+        #     its(['admin']) { should eq(:all_permissions) }
  337
+        #
  338
+        #     # You can still access to its regular methods this way:
  339
+        #     its(:keys) { should include(:max_users) }
  340
+        #     its(:count) { should eq(2) }
  341
+        #   end
  342
+        def its(attribute, &block)
  343
+          describe(attribute) do
  344
+            example do
  345
+              self.class.class_eval do
  346
+                define_method(:subject) do
  347
+                  if defined?(@_subject)
  348
+                    @_subject
  349
+                  else
  350
+                    @_subject = Array === attribute ? super()[*attribute] : _nested_attribute(super(), attribute)
  351
+                  end
  352
+                end
  353
+              end
  354
+              instance_eval(&block)
  355
+            end
  356
+          end
  357
+        end
  358
+      end
  359
+
  360
+      module LetDefinitions
  361
+        # @api private
  362
+        #
  363
+        # Gets the LetDefinitions module. The module is mixed into
  364
+        # the example group and is used to hold all let definitions.
  365
+        # This is done so that the block passed to `let` can be
  366
+        # forwarded directly on to `define_method`, so that all method
  367
+        # constructs (including `super` and `return`) can be used in
  368
+        # a `let` block.
  369
+        #
  370
+        # The memoization is provided by a method definition on the
  371
+        # example group that supers to the LetDefinitions definition
  372
+        # in order to get the value to memoize.
  373
+        def self.module_for(example_group)
  374
+          get_constant_or_yield(example_group, :LetDefinitions) do
  375
+            # Expose `define_method` as a public method, so we can
  376
+            # easily use it below.
  377
+            mod = Module.new { public_class_method :define_method }
  378
+            example_group.__send__(:include, mod)
  379
+            example_group.const_set(:LetDefinitions, mod)
  380
+            mod
  381
+          end
  382
+        end
  383
+
  384
+        if Module.method(:const_defined?).arity == 1 # for 1.8
  385
+          # @api private
  386
+          #
  387
+          # Gets the named constant or yields.
  388
+          # On 1.8, const_defined? / const_get do not take into
  389
+          # account the inheritance hierarchy.
  390
+          def self.get_constant_or_yield(example_group, name)
  391
+            if example_group.const_defined?(name)
  392
+              example_group.const_get(name)
  393
+            else
  394
+              yield
  395
+            end
  396
+          end
  397
+        else
  398
+          # @api private
  399
+          #
  400
+          # Gets the named constant or yields.
  401
+          # On 1.9, const_defined? / const_get take into account the
  402
+          # the inheritance by default, and accept an argument to
  403
+          # disable this behavior. It's important that we don't
  404
+          # consider inheritance here; each example group level that
  405
+          # uses a `let` should get its own `LetDefinitions` module.
  406
+          def self.get_constant_or_yield(example_group, name)
  407
+            if example_group.const_defined?(name, (check_ancestors = false))
  408
+              example_group.const_get(name, check_ancestors)
  409
+            else
  410
+              yield
  411
+            end
  412
+          end
  413
+        end
  414
+      end
  415
+    end
  416
+  end
  417
+end
  418
+
2  lib/rspec/core/shared_context.rb
@@ -18,7 +18,7 @@ module Core
18 18
     #     end
19 19
     module SharedContext
20 20
       include Hooks
21  
-      include Let::ExampleGroupMethods
  21
+      include MemoizedHelpers::ClassMethods
22 22
 
23 23
       def included(group)
24 24
         [:before, :after].each do |type|
260  lib/rspec/core/subject.rb
... ...
@@ -1,260 +0,0 @@
1  
-module RSpec
2  
-  module Core
3  
-    module Subject
4  
-      module ExampleMethods
5  
-        # Returns the example group's `subject`.
6  
-        #
7  
-        # @note `subject` was contributed by Joe Ferris to support the one-liner
8  
-        #   syntax embraced by shoulda matchers:
9  
-        #
10  
-        #       describe Widget do
11  
-        #         it { should validate_presence_of(:name) }
12  
-        #       end
13  
-        #
14  
-        #   While the examples below demonstrate how to use `subject`
15  
-        #   explicitly in examples, we recommend that you define a method with
16  
-        #   an intention revealing name instead.
17  
-        #
18  
-        # @example
19  
-        #
20  
-        #   # explicit declaration of subject
21  
-        #   describe Person do
22  
-        #     subject { Person.new(:birthdate => 19.years.ago) }
23  
-        #     it "should be eligible to vote" do
24  
-        #       subject.should be_eligible_to_vote
25  
-        #       # ^ ^ explicit reference to subject not recommended
26  
-        #     end
27  
-        #   end
28  
-        #
29  
-        #   # implicit subject => { Person.new }
30  
-        #   describe Person do
31  
-        #     it "should be eligible to vote" do
32  
-        #       subject.should be_eligible_to_vote
33  
-        #       # ^ ^ explicit reference to subject not recommended
34  
-        #     end
35  
-        #   end
36  
-        #
37  
-        #   # one-liner syntax - should is invoked on subject
38  
-        #   describe Person do
39  
-        #     it { should be_eligible_to_vote }
40  
-        #   end
41  
-        #
42  
-        # @see ExampleGroupMethods#subject
43  
-        # @see #should
44  
-        def subject
45  
-          # This logic defines an implicit subject.
46  
-          # Explicit `subject` declarations re-define this method.
47  
-          described = described_class || self.class.description
48  
-          Class === described ? described.new : described
49  
-        end
50  
-
51  
-        # When `should` is called with no explicit receiver, the call is
52  
-        # delegated to the object returned by `subject`. Combined with an
53  
-        # implicit subject this supports very concise expressions.
54  
-        #
55  
-        # @example
56  
-        #
57  
-        #   describe Person do
58  
-        #     it { should be_eligible_to_vote }
59  
-        #   end
60  
-        #
61  
-        # @see #subject
62  
-        def should(matcher=nil, message=nil)
63  
-          RSpec::Expectations::PositiveExpectationHandler.handle_matcher(subject, matcher, message)
64  
-        end
65  
-
66  
-        # Just like `should`, `should_not` delegates to the subject (implicit or
67  
-        # explicit) of the example group.
68  
-        #
69  
-        # @example
70  
-        #
71  
-        #   describe Person do
72  
-        #     it { should_not be_eligible_to_vote }
73  
-        #   end
74  
-        #
75  
-        # @see #subject
76  
-        def should_not(matcher=nil, message=nil)
77  
-          RSpec::Expectations::NegativeExpectationHandler.handle_matcher(subject, matcher, message)
78  
-        end
79  
-
80  
-        private
81  
-
82  
-        def _attribute_chain(attribute)
83  
-          attribute.to_s.split('.')
84  
-        end
85  
-
86  
-        def _nested_attribute(subject, attribute)
87  
-          _attribute_chain(attribute).inject(subject) do |inner_subject, attr|
88  
-            inner_subject.send(attr)
89  
-          end
90  
-        end
91  
-      end
92  
-
93  
-      module ExampleGroupMethods
94  
-        # Creates a nested example group named by the submitted `attribute`,
95  
-        # and then generates an example using the submitted block.
96  
-        #
97  
-        # @example
98  
-        #
99  
-        #   # This ...
100  
-        #   describe Array do
101  
-        #     its(:size) { should eq(0) }
102  
-        #   end
103  
-        #
104  
-        #   # ... generates the same runtime structure as this:
105  
-        #   describe Array do
106  
-        #     describe "size" do
107  
-        #       it "should eq(0)" do
108  
-        #         subject.size.should eq(0)
109  
-        #       end
110  
-        #     end
111  
-        #   end
112  
-        #
113  
-        # The attribute can be a `Symbol` or a `String`. Given a `String`
114  
-        # with dots, the result is as though you concatenated that `String`
115  
-        # onto the subject in an expression.
116  
-        #
117  
-        # @example
118  
-        #
119  
-        #   describe Person do
120  
-        #     subject do
121  
-        #       Person.new.tap do |person|
122  
-        #         person.phone_numbers << "555-1212"
123  
-        #       end
124  
-        #     end
125  
-        #
126  
-        #     its("phone_numbers.first") { should eq("555-1212") }
127  
-        #   end
128  
-        #
129  
-        # When the subject is a `Hash`, you can refer to the Hash keys by
130  
-        # specifying a `Symbol` or `String` in an array.
131  
-        #
132  
-        # @example
133  
-        #
134  
-        #   describe "a configuration Hash" do
135  
-        #     subject do
136  
-        #       { :max_users => 3,
137  
-        #         'admin' => :all_permissions }
138  
-        #     end
139  
-        #
140  
-        #     its([:max_users]) { should eq(3) }
141  
-        #     its(['admin']) { should eq(:all_permissions) }
142  
-        #
143  
-        #     # You can still access to its regular methods this way:
144  
-        #     its(:keys) { should include(:max_users) }
145  
-        #     its(:count) { should eq(2) }
146  
-        #   end
147  
-        def its(attribute, &block)
148  
-          describe(attribute) do
149  
-            example do
150  
-              self.class.class_eval do
151  
-                define_method(:subject) do
152  
-                  if defined?(@_subject)
153  
-                    @_subject
154  
-                  else
155  
-                    @_subject = Array === attribute ? super()[*attribute] : _nested_attribute(super(), attribute)
156  
-                  end
157  
-                end
158  
-              end
159  
-              instance_eval(&block)
160  
-            end
161  
-          end
162  
-        end
163  
-
164  
-        # Declares a `subject` for an example group which can then be the
165  
-        # implicit receiver (through delegation) of calls to `should`.
166  
-        #
167  
-        # Given a `name`, defines a method with that name which returns the
168  
-        # `subject`. This lets you declare the subject once and access it
169  
-        # implicitly in one-liners and explicitly using an intention revealing
170  
-        # name.
171  
-        #
172  
-        # @param [String,Symbol] name used to define an accessor with an
173  
-        #   intention revealing name
174  
-        # @param block defines the value to be returned by `subject` in examples
175  
-        #
176  
-        # @example
177  
-        #
178  
-        #   describe CheckingAccount, "with $50" do
179  
-        #     subject { CheckingAccount.new(Money.new(50, :USD)) }
180  
-        #     it { should have_a_balance_of(Money.new(50, :USD)) }
181  
-        #     it { should_not be_overdrawn }
182  
-        #   end
183  
-        #
184  
-        #   describe CheckingAccount, "with a non-zero starting balance" do
185  
-        #     subject(:account) { CheckingAccount.new(Money.new(50, :USD)) }
186  
-        #     it { should_not be_overdrawn }
187  
-        #     it "has a balance equal to the starting balance" do
188  
-        #       account.balance.should eq(Money.new(50, :USD))
189  
-        #     end
190  
-        #   end
191  
-        #
192  
-        # @see ExampleMethods#subject
193  
-        # @see ExampleMethods#should
194  
-        def subject(name=nil, &block)
195  
-          let(:subject, &block)
196  
-          alias_method name, :subject if name
197  
-        end
198  
-
199  
-        # Just like `subject`, except the block is invoked by an implicit `before`
200  
-        # hook. This serves a dual purpose of setting up state and providing a
201  
-        # memoized reference to that state.
202  
-        #
203  
-        # @example
204  
-        #
205  
-        #   class Thing
206  
-        #     def self.count
207  
-        #       @count ||= 0
208  
-        #     end
209  
-        #
210  
-        #     def self.count=(val)
211  
-        #       @count += val
212  
-        #     end
213  
-        #
214  
-        #     def self.reset_count
215  
-        #       @count = 0
216  
-        #     end
217  
-        #
218  
-        #     def initialize
219  
-        #       self.class.count += 1
220  
-        #     end
221  
-        #   end
222  
-        #
223  
-        #   describe Thing do
224  
-        #     after(:each) { Thing.reset_count }
225  
-        #
226  
-        #     context "using subject" do
227  
-        #       subject { Thing.new }
228  
-        #
229  
-        #       it "is not invoked implicitly" do
230  
-        #         Thing.count.should eq(0)
231  
-        #       end
232  
-        #
233  
-        #       it "can be invoked explicitly" do
234  
-        #         subject
235  
-        #         Thing.count.should eq(1)
236  
-        #       end
237  
-        #     end
238  
-        #
239  
-        #     context "using subject!" do
240  
-        #       subject!(:thing) { Thing.new }
241  
-        #
242  
-        #       it "is invoked implicitly" do
243  
-        #         Thing.count.should eq(1)
244  
-        #       end
245  
-        #
246  
-        #       it "returns memoized version on first invocation" do
247  
-        #         subject
248  
-        #         Thing.count.should eq(1)
249  
-        #       end
250  
-        #     end
251  
-        #   end
252  
-        def subject!(name=nil, &block)
253  
-          subject(name, &block)
254  
-          before { __send__(:subject) }
255  
-        end
256  
-      end
257  
-    end
258  
-  end
259  
-end
260  
-
76  spec/rspec/core/let_spec.rb
... ...
@@ -1,76 +0,0 @@
1  
-require 'spec_helper'
2  
-
3  
-describe "#let" do
4  
-  let(:counter) do
5  
-    Class.new do
6  
-      def initialize
7  
-        @count = 0
8  
-      end
9  
-      def count
10  
-        @count += 1
11  
-      end
12  
-    end.new
13  
-  end
14  
-
15  
-  let(:nil_value) do
16  
-    @nil_value_count += 1
17  
-    nil
18  
-  end
19  
-
20  
-  it "generates an instance method" do
21  
-    expect(counter.count).to eq(1)
22  
-  end
23  
-
24  
-  it "caches the value" do
25  
-    expect(counter.count).to eq(1)
26  
-    expect(counter.count).to eq(2)
27  
-  end
28  
-
29  
-  it "caches a nil value" do
30  
-    @nil_value_count = 0
31  
-    nil_value
32  
-    nil_value
33  
-
34  
-    expect(@nil_value_count).to eq(1)
35  
-  end
36  
-
37  
-  let(:a_value) { "a string" }
38  
-
39  
-  context 'when overriding let in a nested context' do
40  
-    let(:a_value) { super() + " (modified)" }
41  
-
42  
-    it 'can use `super` to reference the parent context value' do
43  
-      expect(a_value).to eq("a string (modified)")
44  
-    end
45  
-  end
46  
-
47  
-  context 'when the declaration uses `return`' do
48  
-    let(:value) do
49  
-      return :early_exit if @early_exit
50  
-      :late_exit
51  
-    end
52  
-
53  
-    it 'can exit the let declaration early' do
54  
-      @early_exit = true
55  
-      expect(value).to eq(:early_exit)
56  
-    end
57  
-
58  
-    it 'can get past a conditional `return` statement' do
59  
-      @early_exit = false
60  
-      expect(value).to eq(:late_exit)
61  
-    end
62  
-  end
63  
-end
64  
-
65  
-describe "#let!" do
66  
-  subject { [1,2,3] }
67  
-  let!(:popped) { subject.pop }
68  
-
69  
-  it "evaluates the value non-lazily" do
70  
-    expect(subject).to eq([1,2])
71  
-  end
72  
-
73  
-  it "returns memoized value from first invocation" do
74  
-    expect(popped).to eq(3)
75  
-  end
76  
-end
107  spec/rspec/core/subject_spec.rb → spec/rspec/core/memoized_helpers_spec.rb
... ...
@@ -1,8 +1,7 @@
1 1
 require 'spec_helper'
2 2
 
3 3
 module RSpec::Core
4  
-
5  
-  describe Subject do
  4
+  describe MemoizedHelpers do