once-ler

once-ler supercharges your lets and befores with the performance of before(:all). You get the performance of fixtures without all the headaches.

Setup

Add it to your Gemfile

gem "once-ler"

And then in spec_helper.rb (or wherever):

RSpec.configure do |config|
  config.include Onceler::BasicHelpers
end

Basic usage

before(:once) { ... }

Change a slow before to before(:once) to speed it up.

let_once(...) { ... }

Change a slow let (or let!) to let_once to speed it up.

subject_once(...) { ... }

Change a slow subject (or subject!) to subject_once to speed it up.

around(:once) { ... }

Need to run something around all of your once blocks (like freezing time)? Use this.

around(:once_and_each) { ... }

Shorthand for around(:once) + around(:each). Be careful, as you cannot share state with instance variables between arounds and examples, so use local variables if you want to say freeze time consistently between all before(*) and examples. Also, the block is called twice (once for :once and once for :each), so be careful using, e.g. Time.now in the block.

Ambitious usage

If you're feeling bold, you can automatically speed up all lets/befores in an example group:

describe "something" do
  onceler!

  let(:foo) { ... }      # behaves like let_once
  before { ... }         # behaves like before(:once)

  # but if you need explict eaches, you can still do them:
  let_each(:foo) { ... }
  before(:each) { ... }
end

Configuration

once-ler adds two new before/after scopes, :record and :reset, in case you need to implement additional logic. :record runs in conjunction with once-ler's recording phase (the implicit before :all). :reset runs in conjunction with its cleanup phase (database rollback, implicit after :all). As opposed to :all/:context hooks, these ones are inherited down, and will run before/after any nested once-ler setup/ teardown.

They can be used globally, or in a particular group. For example:

Onceler.configure do |config|
  config.before :record do
    # reset some caching to ensure all recordings get a blank slate
  end
end

describe Foo do
  before(:record) { Foo.stubs(:enabled?).returns(true) }
  after(:record)  { Foo.unstub(:enabled?) } # can't serialize stubs
  before(:each)   { Foo.stubs(:enabled?).returns(true) }

  context "lol" do
    before :once do
      # something that needs the stub to work
    end
    # ...
  end

  context "wut" do
    it do
      # something that needs the stub to work
    end
  end

  # ...
end

How much of a speedup will I get?

YMMV, it depends on how bad your lets/befores are. For example, adding once-ler to a subset of canvas-lms's model specs (spec/models/a*) reduces their runtime by 40%.

How does it work?

Any before(:once)/let_once blocks will run just once for the current context/describe block, before any of its examples run. Any side effects (ivars) and return values will be recorded, and will then be reapplied before each spec in the block runs. once-ler uses nested transactions (savepoints) to ensure that specs don't mess with each other's database rows.

This can give you a dramatic speedup, since you can minimize the number of activerecord callbacks/inserts/updates.

Caveats

  • If you are doing anything database-y, you need to use transactional tests (either via use_transactional_fixtures=true, or something like database_cleaner)
  • Your once'd blocks should have no side effects other than database statements, return values, and instance variables.
  • Your return values and instance variables:
    1. need to be able to handle a Marshal.dump/load round trip. RSpec mocks/doubles can't.
    2. should implement #==. For built-ins types (e.g. String) or models, this isn't a problem, but if it's a custom class you might need to go override Object#==
  • Your once'd blocks' behavior should not depend on side effects of other non-once'd blocks. For example:
    • a before(:once) block should not reference instance variables set by a before (but the inverse is fine).
    • a let_once block should not call non-once'd lets or subjects.
  • Because all let_onces will be recorded and replayed (even if not used in a particular example), you should ensure they don't conflict with each other (e.g. unique constraint violations, or one let_once mutating the return value of another).