Skip to content

Instantly share code, notes, and snippets.

@erik-megarad
Created November 11, 2014 18:55
Show Gist options
  • Save erik-megarad/5df43818f1058f380631 to your computer and use it in GitHub Desktop.
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
@erik-megarad
Copy link
Author

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.

@myronmarston
Copy link

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.

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