module RSpec::Core::MemoizedHelpers::ClassMethods

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.

Public Instance Methods

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.

@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.

@example

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
Calls superclass method
# File rspec-core/lib/rspec/core/memoized_helpers.rb, line 306
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?

  # A list of reserved words that can't be used as a name for a memoized helper
  # Matches for both symbols and passed strings
  if [:initialize, :to_s].include?(name.to_sym)
    raise ArgumentError, "#let or #subject called with reserved name `#{name}`"
  end

  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

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.

@example

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
# File rspec-core/lib/rspec/core/memoized_helpers.rb, line 400
def let!(name, &block)
  let(name, &block)
  before { __send__(name) }
end

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.

@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`

@param name [String,Symbol] used to define an accessor with an

intention revealing name

@param block defines the value to be returned by ‘subject` in examples

@example

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
    account.balance.should eq(Money.new(50, :USD))
  end
end

@see MemoizedHelpers#should @see MemoizedHelpers#should_not @see MemoizedHelpers#is_expected

# File rspec-core/lib/rspec/core/memoized_helpers.rb, line 444
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

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.

@example

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
# File rspec-core/lib/rspec/core/memoized_helpers.rb, line 510
def subject!(name=nil, &block)
  subject(name, &block)
  before { subject }
end