Closed postmodern closed 11 years ago
Also happens in spec/worldlist_spec.rb.
1) Ronin::Wordlist#each_word with wordlist file should enumerate over the words Failure/Error: subject.each_word.to_a.should == words NoMethodError: undefined method `each_word' for Ronin::Wordlist:Class # ./spec/wordlist_spec.rb:97:in `block (4 levels) in' 2) Ronin::Wordlist#each_word with words should enumerate over the words Failure/Error: subject.each_word.to_a.should == words NoMethodError: undefined method `each_word' for Ronin::Wordlist:Class # ./spec/wordlist_spec.rb:105:in `block (4 levels) in ' 3) Ronin::Wordlist#each should rewind file lists Failure/Error: subject.each { |word| } NoMethodError: undefined method `each' for Ronin::Wordlist:Class # ./spec/wordlist_spec.rb:112:in `block (3 levels) in ' 4) Ronin::Wordlist#each_n_words should enumerate over every combination of N words Failure/Error: subject.each_n_words(2).to_a.should == %w[ NoMethodError: undefined method `each_n_words' for Ronin::Wordlist:Class # ./spec/wordlist_spec.rb:127:in `block (3 levels) in ' 5) Ronin::Wordlist#save should save the words with mutations to a file Failure/Error: subject.save(saved_path) NoMethodError: undefined method `save' for Ronin::Wordlist:Class # ./spec/wordlist_spec.rb:139:in `block (3 levels) in '
The describe blocks appear to not be inheriting the top-level subject { }
.
Thanks for reporting this. I'll take a look soon.
After digging into this, I've discovered that it's due to interplay between let
/subject
memoization and before(:all)
. Here's a simpler example that fails:
describe Array do
before(:all) { subject }
describe "Class Methods" do
subject { described_class }
example { expect(subject).to be(Array) }
end
describe "Instance Methods" do
subject { described_class.new([1, 2]) }
example { expect(subject).to be_an(Array) }
end
end
Output:
Array
Class Methods
should equal Array (FAILED - 1)
Instance Methods
should be a kind of Array
Failures:
1) Array Class Methods
Failure/Error: example { expect(subject).to be(Array) }
expected #<Class:70325266257580> => Array
got #<Array:70325274767720> => []
Compared using equal?, which compares object identity,
but expected and actual are not the same object. Use
`expect(actual).to eq(expected)` if you don't care about
object identity in this example.
Diff:
@@ -1,2 +1,2 @@
-Array
+[]
If you comment out the before(:all)
, this passes. Here's another example that uses let
instead of subject
and similarly fails, but passes if before(:all)
is removed:
describe "Let memoization" do
before(:all) { foo }
let(:foo) { 5 }
describe "Class Methods" do
let(:bar) { 3 }
example { expect(bar).to eq(3) }
end
describe "Instance Methods" do
let(:bar) { 4 }
example { expect(bar).to eq(4) }
end
end
Output:
Let memoization
group 1
should eq 3
group 2
should eq 4 (FAILED - 1)
Failures:
1) Let memoization group 2
Failure/Error: example { expect(bar).to eq(4) }
expected: 4
got: 3
(compared using ==)
The source of the problem is that instance variables that are set by code running in before(:all)
get stored and re-assigned for each example, and that includes the @__memoized
variable that is used by let
declarations. As of 19dd6b2d4b25da0042f80bb8ff854672b913ba30 subject
is implemented in terms of let
, so that if @__memoized
gets stored in before(:all)
, these weird bugs occur.
I tried the second example on rspec 2.12 and 2.11, and it fails on those versions as well, so this has been a long-standing bug in rspec. Actually, I don't think the lifecycle of let
-memoized objects when referenced in before(:all)
has ever been specified. We should decide what the behavior should be.
A couple ideas:
let
methods from being called in before(:all)
.let
memoization before every example -- which would imply that the value memoized in before(:all)
will not be preserved for any of the examples.let
memoizations to survive for the duration of whatever scope they are first referenced in--so if a let
declaration is first referenced in before(:all)
it would survive for all the examples in that group.I'm leaning towards the second option, and also adding a warning (something like "WARNING: let
declaration foo
referenced in before(:all)
. The value will not be memoized since this is outside the scope of one example.")
@dchelimsky / @alindeman -- what are your thoughts here?
I don't think a let should be permissible inside a before(:all)
. There's some dissonance in allowing it in some before
calls and not others - this leads me to think that they might belong as separate methods rather than one method that takes a symbol parameter indicating the type of hook it is. Changing method names would take a whole deprecation/removal cycle, not to mention changing something that has been baked in for so long could grate on people, so I understand if that's not really on the table here. Bottom line, I think the most clear behavior is to not allow it in before(:all)
If you go with 2, I would only say there certainly should be a warning in that case since it can result in surprising behavior. The copy you came up with for that is good.
Disallow let methods from being called in before(:all).
Given that let
is normally memoized on a test-by-test basis, referencing it in before(:all)
doesn't make sense at all to me.
If we were starting from scratch, I'd go with "Disallow let methods from being called in before(:all)."
Because it has unintentionally(?) worked in previous versions, though, I think we should go with "Clear let memoization before every example -- which would imply that the value memoized in before(:all) will not be preserved for any of the examples." + a warning.
@alindeman -- that's 100% my opinion, too. I'll take a stab at implementing option 2 + a warning.
BTW, I had forgotten that this had come up so many times before, but we've previously discussed this in #500, #656, #718 and now in this issue. I think the plan we have now (which I've started on, and have a good idea how to implement) is the right approach, and it's in line with our previous discussions.
Ran some specs that make heavy use of
subject { }
against rspec 2.13.x. It appears to be ignoring thesubject { }
blocks, and usingdescribed_class
instead.