lib/rspec/core/memoized_helpers.rb
RSpec::Support.require_rspec_support 'reentrant_mutex'
module RSpec
module Core
# This module is included in {ExampleGroup}, making the methods
# available to be called from within example blocks.
#
# @see ClassMethods
module MemoizedHelpers
# @note `subject` was contributed by Joe Ferris to support the one-liner
# syntax embraced by shoulda matchers:
#
# RSpec.describe Widget do
# it { is_expected.to validate_presence_of(:name) }
# # or
# it { should validate_presence_of(:name) }
# end
#
# While the examples below demonstrate how to use `subject`
# explicitly in examples, we recommend that you define a method with
# an intention revealing name instead.
#
# @example
#
# # Explicit declaration of subject.
# RSpec.describe Person do
# subject { Person.new(:birthdate => 19.years.ago) }
# it "should be eligible to vote" do
# subject.should be_eligible_to_vote
# # ^ ^ explicit reference to subject not recommended
# end
# end
#
# # Implicit subject => { Person.new }.
# RSpec.describe Person do
# it "should be eligible to vote" do
# subject.should be_eligible_to_vote
# # ^ ^ explicit reference to subject not recommended
# end
# end
#
# # One-liner syntax - expectation is set on the subject.
# RSpec.describe Person do
# it { is_expected.to be_eligible_to_vote }
# # or
# it { should be_eligible_to_vote }
# end
#
# @note Because `subject` 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,
# `subject` is _not_ intended to be used in a `before(:context)` hook.
#
# @see #should
# @see #should_not
# @see #is_expected
def subject
__memoized.fetch_or_store(:subject) do
described = described_class || self.class.metadata.fetch(:description_args).first
Class === described ? described.new : described
end
end
# When `should` is called with no explicit receiver, the call is
# delegated to the object returned by `subject`. Combined with an
# implicit subject this supports very concise expressions.
#
# @example
#
# RSpec.describe Person do
# it { should be_eligible_to_vote }
# end
#
# @see #subject
# @see #is_expected
#
# @note This only works if you are using rspec-expectations.
# @note If you are using RSpec's newer expect-based syntax you may
# want to use `is_expected.to` instead of `should`.
def should(matcher=nil, message=nil)
RSpec::Expectations::PositiveExpectationHandler.handle_matcher(subject, matcher, message)
end
# Just like `should`, `should_not` delegates to the subject (implicit or
# explicit) of the example group.
#
# @example
#
# RSpec.describe Person do
# it { should_not be_eligible_to_vote }
# end
#
# @see #subject
# @see #is_expected
#
# @note This only works if you are using rspec-expectations.
# @note If you are using RSpec's newer expect-based syntax you may
# want to use `is_expected.to_not` instead of `should_not`.
def should_not(matcher=nil, message=nil)
RSpec::Expectations::NegativeExpectationHandler.handle_matcher(subject, matcher, message)
end
# Wraps the `subject` in `expect` to make it the target of an expectation.
# Designed to read nicely for one-liners.
#
# @example
#
# describe [1, 2, 3] do
# it { is_expected.to be_an Array }
# it { is_expected.not_to include 4 }
# end
#
# @see #subject
# @see #should
# @see #should_not
#
# @note This only works if you are using rspec-expectations.
def is_expected
expect(subject)
end
# @private
# should just be placed in private section,
# but Ruby issues warnings on private attributes.
# and expanding it to the equivalent method upsets Rubocop,
# b/c it should obviously be a reader
attr_reader :__memoized
private :__memoized
private
# @private
def initialize(*)
__init_memoized
super
end
# @private
def __init_memoized
@__memoized = if RSpec.configuration.threadsafe?
ThreadsafeMemoized.new
else
NonThreadSafeMemoized.new
end
end
# @private
class ThreadsafeMemoized
def initialize
@memoized = {}
@mutex = Support::ReentrantMutex.new
end
def fetch_or_store(key)
@memoized.fetch(key) do # only first access pays for synchronization
@mutex.synchronize do
@memoized.fetch(key) { @memoized[key] = yield }
end
end
end
end
# @private
class NonThreadSafeMemoized
def initialize
@memoized = {}
end
def fetch_or_store(key)
@memoized.fetch(key) { @memoized[key] = yield }
end
end
# Used internally to customize the behavior of the
# memoized hash when used in a `before(:context)` hook.
#
# @private
class ContextHookMemoized
def self.isolate_for_context_hook(example_group_instance)
exploding_memoized = self
example_group_instance.instance_exec do
@__memoized = exploding_memoized
begin
yield
ensure
# This is doing a reset instead of just isolating for context hook.
# Really, this should set the old @__memoized back into place.
#
# Caller is the before and after context hooks
# which are both called from self.run
# I didn't look at why it made tests fail, maybe an object was getting reused in RSpec tests,
# if so, then that probably already works, and its the tests that are wrong.
__init_memoized
end
end
end
def self.fetch_or_store(key, &_block)
description = if key == :subject
"subject"
else
"let declaration `#{key}`"
end
raise <<-EOS
#{description} accessed in #{article} #{hook_expression} hook at:
#{CallerFilter.first_non_rspec_line}
`let` and `subject` declarations are not intended to be called
in #{article} #{hook_expression} hook, as they exist to define state that
is reset between each example, while #{hook_expression} exists to
#{hook_intention}.
EOS
end
# @private
class Before < self
def self.hook_expression
"`before(:context)`"
end
def self.article
"a"
end
def self.hook_intention
"define state that is shared across examples in an example group"
end
end
# @private
class After < self
def self.hook_expression
"`after(:context)`"
end
def self.article
"an"
end
def self.hook_intention
"cleanup state that is shared across examples in an example group"
end
end
end
# 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.
module ClassMethods
# 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
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
# 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
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
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
def subject!(name=nil, &block)
subject(name, &block)
before { subject }
end
end
# @private
#
# Gets the LetDefinitions module. The module is mixed into
# the example group and is used to hold all let definitions.
# This is done so that the block passed to `let` can be
# forwarded directly on to `define_method`, so that all method
# constructs (including `super` and `return`) can be used in
# a `let` block.
#
# The memoization is provided by a method definition on the
# example group that supers to the LetDefinitions definition
# in order to get the value to memoize.
def self.module_for(example_group)
get_constant_or_yield(example_group, :LetDefinitions) do
mod = Module.new do
include(Module.new {
example_group.const_set(:NamedSubjectPreventSuper, self)
})
end
example_group.const_set(:LetDefinitions, mod)
mod
end
end
# @private
def self.define_helpers_on(example_group)
example_group.__send__(:include, module_for(example_group))
end
if Module.method(:const_defined?).arity == 1 # for 1.8
# @private
#
# Gets the named constant or yields.
# On 1.8, const_defined? / const_get do not take into
# account the inheritance hierarchy.
# :nocov:
def self.get_constant_or_yield(example_group, name)
if example_group.const_defined?(name)
example_group.const_get(name)
else
yield
end
end
# :nocov:
else
# @private
#
# Gets the named constant or yields.
# On 1.9, const_defined? / const_get take into account the
# the inheritance by default, and accept an argument to
# disable this behavior. It's important that we don't
# consider inheritance here; each example group level that
# uses a `let` should get its own `LetDefinitions` module.
def self.get_constant_or_yield(example_group, name)
if example_group.const_defined?(name, (check_ancestors = false))
example_group.const_get(name, check_ancestors)
else
yield
end
end
end
end
end
end