Module: RSpec::Core::MemoizedHelpers::ClassMethods

Included in:
ExampleGroup
Defined in:
lib/rspec/core/memoized_helpers.rb

Overview

This module is extended onto ExampleGroup, making the methods available to be called from within example group blocks. You can think of them as being analagous to class macros.

Instance Method Summary collapse

Instance Method Details

#let(name, &block) ⇒ void

Note:

let can enhance readability when used sparingly (1,2, or maybe 3 declarations) in any given example group, but that can quickly degrade with overuse. YMMV.

Note:

let can be configured to be threadsafe or not. If it is threadsafe, it will take longer to access the value. If it is not threadsafe, it may behave in surprising ways in examples that spawn separate threads. Specify this on RSpec.configure

Note:

Because let is designed to create state that is reset between each example, and before(:context) is designed to setup state that is shared across all examples in an example group, let is not intended to be used in a before(:context) hook.

Generates a method whose return value is memoized after the first call. Useful for reducing duplication between examples that assign values to the same local variable.

Examples:


RSpec.describe Thing do
  let(:thing) { Thing.new }

  it "does something" do
    # First invocation, executes block, memoizes and returns result.
    thing.do_something

    # Second invocation, returns the memoized value.
    thing.should be_something
  end
end

284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
# File 'lib/rspec/core/memoized_helpers.rb', line 284

def let(name, &block)
  # We have to pass the block directly to `define_method` to
  # allow it to use method constructs like `super` and `return`.
  raise "#let or #subject called without a block" if block.nil?
  raise(
    "#let or #subject called with a reserved name #initialize"
  ) if :initialize == name
  our_module = MemoizedHelpers.module_for(self)

  # If we have a module clash in our helper module
  # then we need to remove it to prevent a warning.
  #
  # Note we do not check ancestor modules (see: `instance_methods(false)`)
  # as we can override them.
  if our_module.instance_methods(false).include?(name)
    our_module.__send__(:remove_method, name)
  end
  our_module.__send__(:define_method, name, &block)

  # If we have a module clash in the example module
  # then we need to remove it to prevent a warning.
  #
  # Note we do not check ancestor modules (see: `instance_methods(false)`)
  # as we can override them.
  if instance_methods(false).include?(name)
    remove_method(name)
  end

  # Apply the memoization. The method has been defined in an ancestor
  # module so we can use `super` here to get the value.
  if block.arity == 1
    define_method(name) { __memoized.fetch_or_store(name) { super(RSpec.current_example, &nil) } }
  else
    define_method(name) { __memoized.fetch_or_store(name) { super(&nil) } }
  end
end

#let!(name, &block) ⇒ void

Just like let, except the block is invoked by an implicit before hook. This serves a dual purpose of setting up state and providing a memoized reference to that state.

Examples:


class Thing
  def self.count
    @count ||= 0
  end

  def self.count=(val)
    @count += val
  end

  def self.reset_count
    @count = 0
  end

  def initialize
    self.class.count += 1
  end
end

RSpec.describe Thing do
  after(:example) { Thing.reset_count }

  context "using let" do
    let(:thing) { Thing.new }

    it "is not invoked implicitly" do
      Thing.count.should eq(0)
    end

    it "can be invoked explicitly" do
      thing
      Thing.count.should eq(1)
    end
  end

  context "using let!" do
    let!(:thing) { Thing.new }

    it "is invoked implicitly" do
      Thing.count.should eq(1)
    end

    it "returns memoized version on first invocation" do
      thing
      Thing.count.should eq(1)
    end
  end
end

374
375
376
377
# File 'lib/rspec/core/memoized_helpers.rb', line 374

def let!(name, &block)
  let(name, &block)
  before { __send__(name) }
end

#subject(name = nil, &block) ⇒ void

Note:

subject can be configured to be threadsafe or not. If it is threadsafe, it will take longer to access the value. If it is not threadsafe, it may behave in surprising ways in examples that spawn separate threads. Specify this on RSpec.configure

Declares a subject for an example group which can then be wrapped with expect using is_expected to make it the target of an expectation in a concise, one-line example.

Given a name, defines a method with that name which returns the subject. This lets you declare the subject once and access it implicitly in one-liners and explicitly using an intention revealing name.

When given a name, calling super in the block is not supported.

Examples:


RSpec.describe CheckingAccount, "with $50" do
  subject { CheckingAccount.new(Money.new(50, :USD)) }
  it { is_expected.to have_a_balance_of(Money.new(50, :USD)) }
  it { is_expected.not_to be_overdrawn }
end

RSpec.describe CheckingAccount, "with a non-zero starting balance" do
  subject(:account) { CheckingAccount.new(Money.new(50, :USD)) }
  it { is_expected.not_to be_overdrawn }
  it "has a balance equal to the starting balance" do
    .balance.should eq(Money.new(50, :USD))
  end
end

Parameters:

  • name (String, Symbol) (defaults to: nil)

    used to define an accessor with an intention revealing name

  • block

    defines the value to be returned by subject in examples

See Also:


418
419
420
421
422
423
424
425
426
427
428
429
# File 'lib/rspec/core/memoized_helpers.rb', line 418

def subject(name=nil, &block)
  if name
    let(name, &block)
    alias_method :subject, name

    self::NamedSubjectPreventSuper.__send__(:define_method, name) do
      raise NotImplementedError, "`super` in named subjects is not supported"
    end
  else
    let(:subject, &block)
  end
end

#subject!(name = nil, &block) ⇒ void

Just like subject, except the block is invoked by an implicit before hook. This serves a dual purpose of setting up state and providing a memoized reference to that state.

Examples:


class Thing
  def self.count
    @count ||= 0
  end

  def self.count=(val)
    @count += val
  end

  def self.reset_count
    @count = 0
  end

  def initialize
    self.class.count += 1
  end
end

RSpec.describe Thing do
  after(:example) { Thing.reset_count }

  context "using subject" do
    subject { Thing.new }

    it "is not invoked implicitly" do
      Thing.count.should eq(0)
    end

    it "can be invoked explicitly" do
      subject
      Thing.count.should eq(1)
    end
  end

  context "using subject!" do
    subject!(:thing) { Thing.new }

    it "is invoked implicitly" do
      Thing.count.should eq(1)
    end

    it "returns memoized version on first invocation" do
      subject
      Thing.count.should eq(1)
    end
  end
end

484
485
486
487
# File 'lib/rspec/core/memoized_helpers.rb', line 484

def subject!(name=nil, &block)
  subject(name, &block)
  before { subject }
end