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
# 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