-
-
Save erik-megarad/5df43818f1058f380631 to your computer and use it in GitHub Desktop.
describe "A long-running process" do | |
before(:context) do # Have to use before(:context) instead of subject() to make the long-running task only execute once | |
@input_one = 1 | |
@input_two = 2 # Can't use let() in before(:context) | |
@my_test_object = MyTestClass.new | |
@my_test_object.thing_that_takes_thirty_minutes(@input_one, @input_two) | |
end | |
# Multiple assertions | |
it 'calculates the output correctly' do | |
@my_test_object.value.should eq 3 | |
end | |
it 'does not set the failed flag' do | |
@my_test_object.failed.should be_nil | |
end | |
end |
Thanks for the response. I wasn't referring to betterspecs.org, specifically, but the general feeling that using instance variables instead of let
seems like a dirty hack. You also lose some convenient things like memoization, which aren't particularly important in this situation.
I was indeed attempting to follow the "one expectation/assertion per example/test" guideline, mostly because if multiple assertions fail, I want to see each one that failed, instead of just the first. If I want that, I'm forced to use before(:context)
, which also seems like a dirty hack.
Writing tests this way means you leave behind most of the benefits of rspec, in my opinion. Things still "work," but it's no longer pleasant to write. In an ideal world, this would be how this test would look:
describe "A long-running process" do
let(:input_one) { 1 }
let(:input_two) { 2 }
let(:my_test_object) { MyTestClass.new }
subject(:thing_that_takes_thirty_minutes, context: true) do
my_test_object.thing_that_takes_thirty_minutes(input_one, input_two)
end
it 'calculates the output correctly' do
expect { thing_that_takes_thirty_minutes }.to change {
my_test_object.value
}.to(3)
end
it 'does not set the failed flag' do
expect { thing_that_takes_thirty_minutes }.to_not change {
my_test_object.failed
}
end
end
Note how it looks the same as any other test, with the only addition being a flag on subject
(called context
here, but there's probably a better name for it) that instructs rspec to only evaluate the subject
once per context
. That seems like an elegant solution for everybody.
Here's a little extension that provides something very similar to what you're asking for:
module RSpecContextMemoization
def let(name, &block)
attr_reader name
before(:context) { instance_variable_set(:"@#{name}", block.call) }
end
end
RSpec.configure do |config|
config.extend RSpecContextMemoization, memoization_scope: :context
end
Put that in spec/spec_helper.rb
(or somewhere that is always loaded) and then tag example groups in which you want let
/subject
memoization to have context rather than example scope with memoization_scope: :context
. Your example would become:
describe "A long-running process", memoization_scope: :context do
let(:input_one) { 1 }
let(:input_two) { 2 }
let(:my_test_object) { MyTestClass.new }
subject(:thing_that_takes_thirty_minutes) do
my_test_object.thing_that_takes_thirty_minutes(input_one, input_two)
end
it 'calculates the output correctly' do
expect { thing_that_takes_thirty_minutes }.to change {
my_test_object.value
}.to(3)
end
it 'does not set the failed flag' do
expect { thing_that_takes_thirty_minutes }.to_not change {
my_test_object.failed
}
end
end
...and it should just work.
It's trivial to add this functionality on top of what RSpec already provides. We field enough questions where users get confused when misusing before(:context)
hooks that I don't want to add more features to RSpec that would encourage their (mis)use, particularly because it's so easy to add this kind of thing on top of the APIs already provided. It could make a great extension gem, though.
First off, what you've done here is completely fine. It's not incorrect in any way. So I don't really understand what you mean by:
Are you basing that statement on the http://betterspecs.org/ guidelines? Bear in mind the guidelines there do not reflect the opinions of RSpec's core team at all. There are many things there I disagree with.
Anyhow, while what you have here is totally fine, bear in mind that
before(:context)
hooks come with a lot of caveats. (See the "Warning: before(:context)" section of our docs. Many testing "resources" (for lack of a better term) like test doubles and DB transactions have a per-example life cycle, and when you usebefore(:context)
hook, you are operating outside of that life cycle. As long as you don't use those constructs, you'll be fine, but others who work on the code base may not be aware of those caveats--so I generally recommend avoidingbefore(:context)
hooks for things like this. (You certainly canIt looks like you chose to use a
before(:context)
hook to follow the "one expectation/assertion per example/test" guideline? I'd say that that guideline was really only ever meant to be applied to unit tests, where the setup/teardown is extremely minimal, and making separate examples with separate expectations or assertions is very cheap. I would never follow that guideline for anything that has long-running setup. In your case, it looks like this is more of an integration or acceptance test, and I'd just put it all in one example:In this particular case, if you are using RSpec 3.1+, you could replace the two expectations with one:
As for not being able to use
let
frombefore(:context)
blocks: they were never intended to be used frombefore(:context)
blocks. The docs forlet
have always said that they are cleared between each example, which clearly doesn't make fit withbefore(:context)
hooks. In theory, we could make them have a lifecycle relative to whatever construct they are accessed in, but then we'd have to start putting conditionals in our docs, e.g. "if they are accessed from abefore(:context)
hook, they last for the whole example group, otherwise they are cleared between examples" -- which seemed to use to be a huge smell.In RSpec 2, we didn't originally prevent you from calling
let
s frombefore(:all)
hooks, but users reported some nasty bugs that occurred because we didn't explicitly prevent, so we decided to make it explicit.If the bare words syntax is what you're after with using a
let
, you can still get that trivially in yourbefore(:context)
hook...just use `attr_reader:Let me know if you have any more questions.