Skip to content
This repository was archived by the owner on Nov 30, 2024. It is now read-only.

Implicit subject should be memoized with let#768

Merged
myronmarston merged 2 commits intorspec:masterfrom
exviva:subject_not_memoized
Jan 5, 2013
Merged

Implicit subject should be memoized with let#768
myronmarston merged 2 commits intorspec:masterfrom
exviva:subject_not_memoized

Conversation

@exviva
Copy link
Copy Markdown
Contributor

@exviva exviva commented Jan 4, 2013

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.

Comment thread lib/rspec/core/example_group.rb Outdated
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it - misread the comment :)

@myronmarston
Copy link
Copy Markdown
Member

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

@exviva
Copy link
Copy Markdown
Contributor Author

exviva commented Jan 4, 2013

@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?

@myronmarston
Copy link
Copy Markdown
Member

@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.

@exviva
Copy link
Copy Markdown
Contributor Author

exviva commented Jan 4, 2013

@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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do.

@myronmarston
Copy link
Copy Markdown
Member

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...

@exviva
Copy link
Copy Markdown
Contributor Author

exviva commented Jan 5, 2013

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.

@myronmarston myronmarston merged commit 0c70d63 into rspec:master Jan 5, 2013
@myronmarston
Copy link
Copy Markdown
Member

@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.

@exviva exviva deleted the subject_not_memoized branch January 5, 2013 18:13
@myronmarston
Copy link
Copy Markdown
Member

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

@exviva
Copy link
Copy Markdown
Contributor Author

exviva commented Jan 5, 2013

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 :).

@myronmarston
Copy link
Copy Markdown
Member

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?

@myronmarston
Copy link
Copy Markdown
Member

@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?

@exviva
Copy link
Copy Markdown
Contributor Author

exviva commented Jan 7, 2013

@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.

@exviva
Copy link
Copy Markdown
Contributor Author

exviva commented Jan 7, 2013

@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.

@myronmarston
Copy link
Copy Markdown
Member

@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?

@exviva
Copy link
Copy Markdown
Contributor Author

exviva commented Jan 7, 2013

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.

@myronmarston
Copy link
Copy Markdown
Member

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.

@exviva
Copy link
Copy Markdown
Contributor Author

exviva commented Jan 7, 2013

@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?

@myronmarston
Copy link
Copy Markdown
Member

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?

@exviva exviva mentioned this pull request Jan 8, 2013
@exviva
Copy link
Copy Markdown
Contributor Author

exviva commented Jan 8, 2013

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.

@myronmarston
Copy link
Copy Markdown
Member

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 subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants