Skip to content

Commit

Permalink
Implement Deferred::Dsl::MemoizedHelpers#let, #let!
Browse files Browse the repository at this point in the history
  • Loading branch information
sleepingkingstudios committed Jun 1, 2024
1 parent b29a9d9 commit 00bb9dd
Show file tree
Hide file tree
Showing 5 changed files with 322 additions and 0 deletions.
1 change: 1 addition & 0 deletions lib/rspec/sleeping_king_studios/deferred/dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ module Dsl; end
require 'rspec/sleeping_king_studios/deferred/dsl/examples'
require 'rspec/sleeping_king_studios/deferred/dsl/example_groups'
require 'rspec/sleeping_king_studios/deferred/dsl/hooks'
require 'rspec/sleeping_king_studios/deferred/dsl/memoized_helpers'
60 changes: 60 additions & 0 deletions lib/rspec/sleeping_king_studios/deferred/dsl/memoized_helpers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# frozen_string_literal: true

require 'rspec/sleeping_king_studios/deferred/dsl'

module RSpec::SleepingKingStudios::Deferred::Dsl
# DSL for defining memoized helpers for deferred examples.
module MemoizedHelpers
# Callback invoked when the module is extended into another module or class.
#
# Defines a HelperImplementations module on the module and includes it in
# the module.
#
# @param other [Module] the other module or class.
def self.extended(other)
super

return if other.const_defined?(:HelperImplementations, true)

other.const_set(:HelperImplementations, Module.new)
end

def call(example_group)
super

include self::HelperImplementations
end

# Defines a memoized helper.
#
# @param helper_name [String, Symbol] the name of the helper method.
# @param block [Block] the implementation of the helper method.
#
# @return [void]
def let(helper_name, &block)
helper_name = helper_name.to_sym

self::HelperImplementations.define_method(helper_name, &block)

define_method(helper_name) do
helper_values = @memoized_helper_values ||= {}

helper_values.fetch(helper_name) do
helper_values[helper_name] = super()
end
end
end

# Defines a memoized helper and adds a hook to evaluate it before examples.
#
# @param helper_name [String, Symbol] the name of the helper method.
# @param block [Block] the implementation of the helper method.
#
# @return [void]
def let!(helper_name, &block)
let(helper_name, &block)

before(:example) { send(helper_name) }
end
end
end
2 changes: 2 additions & 0 deletions spec/rspec/sleeping_king_studios/deferred/dsl/hooks_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
require 'rspec/sleeping_king_studios/deferred/definitions'
require 'rspec/sleeping_king_studios/deferred/dsl/hooks'

require 'support/shared_examples/deferred_examples'

RSpec.describe RSpec::SleepingKingStudios::Deferred::Dsl::Hooks do
extend RSpec::SleepingKingStudios::Concerns::ExampleConstants
include Spec::Support::SharedExamples::DeferredExamples
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# frozen_string_literal: true

require 'rspec/sleeping_king_studios/concerns/example_constants'
require 'rspec/sleeping_king_studios/deferred/dsl/hooks'
require 'rspec/sleeping_king_studios/deferred/dsl/memoized_helpers'
require 'rspec/sleeping_king_studios/matchers/built_in/respond_to'
require 'rspec/sleeping_king_studios/matchers/core/have_constant'

require 'support/isolated_example_group'

require 'support/shared_examples/deferred_examples'

RSpec.describe RSpec::SleepingKingStudios::Deferred::Dsl::MemoizedHelpers do
extend RSpec::SleepingKingStudios::Concerns::ExampleConstants
include Spec::Support::SharedExamples::DeferredExamples

subject(:definitions) { described_class }

let(:described_class) { Spec::DeferredExamples }
let(:ancestor_class) { Spec::InheritedExamples }
let(:ancestor_examples) { ancestor_class }

example_constant 'Spec::InheritedExamples' do
Module.new do
extend RSpec::SleepingKingStudios::Deferred::Definitions
extend RSpec::SleepingKingStudios::Deferred::Dsl::Hooks
extend RSpec::SleepingKingStudios::Deferred::Dsl::MemoizedHelpers
end
end

example_constant 'Spec::DeferredExamples' do
Module.new do
extend RSpec::SleepingKingStudios::Deferred::Definitions
extend RSpec::SleepingKingStudios::Deferred::Dsl::Hooks
extend RSpec::SleepingKingStudios::Deferred::Dsl::MemoizedHelpers
include Spec::InheritedExamples
end
end

include_examples 'should define memoized helpers'
end
218 changes: 218 additions & 0 deletions spec/support/shared_examples/deferred_examples.rb
Original file line number Diff line number Diff line change
Expand Up @@ -441,5 +441,223 @@ module DeferredExamples
include_examples 'should define a hook macro', :prepend_before
end
end

shared_examples 'should define memoized helpers' do
shared_examples 'should define a memoized helper macro' \
do |method_name, before: false, subject: false|
shared_context 'when the helper is defined' do
before(:example) do
described_class.send(method_name, helper_name, &block)
end
end

shared_examples 'should call and memoize the block value' do
context 'when the helper is defined' do
include_context 'when the helper is defined'

it 'should return the value returned by the block' do
expect(call_helper).to be 0
end

it 'should memoize the value', :aggregate_failures do
3.times { expect(call_helper).to be 0 }
end
end
end

shared_examples 'should call the existing implementation' do
context 'when the helper is defined' do
include_context 'when the helper is defined'

it 'should call the existing implementation' do
expect(call_helper).to be existing_block.call
end
end
end

shared_examples 'should wrap the existing implementation' do
context 'when the helper calls super()' do
include_context 'when the helper is defined'

let(:block) { -> { super() - 1 } }

it 'should return the value returned by the block' do
expect(call_helper).to be(-2)
end

it 'should memoize the value', :aggregate_failures do
3.times { expect(call_helper).to be(-2) }
end
end
end

let(:existing_block) { -> { -1 } }
let(:helper_name) { :payload }
let(:block) do
counter = -1

-> { counter += 1 }
end
let(:parent_group) { Spec::Support.isolated_example_group }
let(:example_group) do
deferred_examples = described_class

Spec::Support.isolated_example_group(parent_group) do
include deferred_examples
end
end
let(:example_instance) { example_group.new }

def call_helper
example_instance.send(helper_name)
end

it 'should define the class method' do
expect(described_class)
.to respond_to(method_name)
.with(1).argument
.and_a_block
end

it 'should define the helper method' do
described_class.send(method_name, helper_name, &block)

expect(example_group.new).to respond_to(helper_name).with(0).arguments
end

include_examples 'should call and memoize the block value'

context 'when a helper is declared in a parent example group' do
before(:example) do
parent_group.let(helper_name, &existing_block)
end

include_examples 'should call and memoize the block value'

include_examples 'should wrap the existing implementation'
end

context 'when a method is declared in a parent example group' do
before(:example) do
parent_group.define_method(helper_name, &existing_block)
end

include_examples 'should call and memoize the block value'

include_examples 'should wrap the existing implementation'
end

context 'when a helper is defined in the example scope' do
before(:example) do
example_group.let(helper_name) { -1 }
end

include_examples 'should call the existing implementation'
end

context 'when a method is defined in the example scope' do
before(:example) do
example_group.define_method(helper_name, &existing_block)
end

include_examples 'should call the existing implementation'
end

context 'when a helper is defined in the same scope' do
before(:example) do
described_class.let(helper_name) { -1 }
end

include_examples 'should call and memoize the block value'
end

context 'when a method is defined in the same scope' do
before(:example) do
described_class.define_method(helper_name) { -1 }
end

include_examples 'should call and memoize the block value'
end

context 'when a helper is defined in inherited examples' do
before(:example) do
ancestor_class.let(helper_name) { -1 }
end

include_examples 'should call and memoize the block value'
end

context 'when a method is defined in inherited examples' do
before(:example) do
ancestor_class.define_method(helper_name) { -1 }
end

include_examples 'should call and memoize the block value'
end

if before
it 'should add a deferred hook to the class' do
expect { described_class.send(method_name, helper_name, &block) }
.to change(described_class, :deferred_hooks)
end

it 'should define a deferred hook', :aggregate_failures do
described_class.send(method_name, helper_name, &block)

deferred = described_class.deferred_hooks.last

expect(deferred)
.to be_a(RSpec::SleepingKingStudios::Deferred::Calls::Hook)
expect(deferred.method_name).to be :before
expect(deferred.scope).to be :example
expect(deferred.arguments).to be == %i[example]
expect(deferred.keywords).to be == {}
expect(deferred.block).to be_a Proc
end

it 'should reference the helper method in the hook' do
described_class.send(method_name, helper_name, &block)

deferred = described_class.deferred_hooks.last
value = example_instance.instance_exec(&deferred.block)

expect(value).to be 0
end
end
end

describe '::HelperImplementations' do
it 'should define the constant' do
expect(described_class)
.to define_constant(:HelperImplementations)
.with_value(an_instance_of Module)
end

it 'should not include the helper implementations' do
expect(described_class)
.not_to be < described_class::HelperImplementations
end
end

describe '#call' do
let(:example_group) { Spec::Support.isolated_example_group }

it 'should include the helper implementations' do
example_group.include(described_class)

expect(described_class).to be < described_class::HelperImplementations
end
end

describe '.let' do
include_examples 'should define a memoized helper macro', :let
end

describe '.let!' do
include_examples 'should define a memoized helper macro',
:let!,
before: true
end
end
end
end

0 comments on commit 00bb9dd

Please sign in to comment.