lib/rspec/core/shared_example_group.rb
RSpec::Support.require_rspec_support "with_keywords_when_needed"
module RSpec
module Core
# Represents some functionality that is shared with multiple example groups.
# The functionality is defined by the provided block, which is lazily
# eval'd when the `SharedExampleGroupModule` instance is included in an example
# group.
class SharedExampleGroupModule < Module
# @private
attr_reader :definition
def initialize(description, definition, metadata)
@description = description
@definition = definition
@metadata = metadata
end
# Provides a human-readable representation of this module.
def inspect
"#<#{self.class.name} #{@description.inspect}>"
end
alias to_s inspect
# Ruby callback for when a module is included in another module is class.
# Our definition evaluates the shared group block in the context of the
# including example group.
def included(klass)
inclusion_line = klass.metadata[:location]
include_in klass, inclusion_line, [], nil
end
# @private
def include_in(klass, inclusion_line, args, customization_block)
klass.update_inherited_metadata(@metadata) unless @metadata.empty?
SharedExampleGroupInclusionStackFrame.with_frame(@description, inclusion_line) do
RSpec::Support::WithKeywordsWhenNeeded.class_exec(klass, *args, &@definition)
klass.class_exec(&customization_block) if customization_block
end
end
end
# Shared example groups let you define common context and/or common
# examples that you wish to use in multiple example groups.
#
# When defined, the shared group block is stored for later evaluation.
# It can later be included in an example group either explicitly
# (using `include_examples`, `include_context` or `it_behaves_like`)
# or implicitly (via matching metadata).
#
# Named shared example groups are scoped based on where they are
# defined. Shared groups defined in an example group are available
# for inclusion in that example group or any child example groups,
# but not in any parent or sibling example groups. Shared example
# groups defined at the top level can be included from any example group.
module SharedExampleGroup
# @overload shared_examples(name, &block)
# @param name [String, Symbol, Module] identifer to use when looking up
# this shared group
# @param block The block to be eval'd
# @overload shared_examples(name, metadata, &block)
# @param name [String, Symbol, Module] identifer to use when looking up
# this shared group
# @param metadata [Array<Symbol>, Hash] metadata to attach to this
# group; any example group or example with matching metadata will
# automatically include this shared example group.
# @param block The block to be eval'd
#
# Stores the block for later use. The block will be evaluated
# in the context of an example group via `include_examples`,
# `include_context`, or `it_behaves_like`.
#
# @example
# shared_examples "auditable" do
# it "stores an audit record on save!" do
# expect { auditable.save! }.to change(Audit, :count).by(1)
# end
# end
#
# RSpec.describe Account do
# it_behaves_like "auditable" do
# let(:auditable) { Account.new }
# end
# end
#
# @see ExampleGroup.it_behaves_like
# @see ExampleGroup.include_examples
# @see ExampleGroup.include_context
def shared_examples(name, *args, &block)
top_level = self == ExampleGroup
if top_level && RSpec::Support.thread_local_data[:in_example_group]
raise "Creating isolated shared examples from within a context is " \
"not allowed. Remove `RSpec.` prefix or move this to a " \
"top-level scope."
end
RSpec.world.shared_example_group_registry.add(self, name, *args, &block)
end
alias shared_context shared_examples
alias shared_examples_for shared_examples
# @api private
#
# Shared examples top level DSL.
module TopLevelDSL
# @private
def self.definitions
proc do
def shared_examples(name, *args, &block)
RSpec.world.shared_example_group_registry.add(:main, name, *args, &block)
end
alias shared_context shared_examples
alias shared_examples_for shared_examples
end
end
# @private
def self.exposed_globally?
@exposed_globally ||= false
end
# @api private
#
# Adds the top level DSL methods to Module and the top level binding.
def self.expose_globally!
return if exposed_globally?
Core::DSL.change_global_dsl(&definitions)
@exposed_globally = true
end
# @api private
#
# Removes the top level DSL methods to Module and the top level binding.
def self.remove_globally!
return unless exposed_globally?
Core::DSL.change_global_dsl do
undef shared_examples
undef shared_context
undef shared_examples_for
end
@exposed_globally = false
end
end
# @private
class Registry
def add(context, name, *metadata_args, &block)
unless block
RSpec.warning "Shared example group #{name} was defined without a "\
"block and will have no effect. Please define a "\
"block or remove the definition."
end
if RSpec.configuration.shared_context_metadata_behavior == :trigger_inclusion
return legacy_add(context, name, *metadata_args, &block)
end
unless valid_name?(name)
raise ArgumentError, "Shared example group names can only be a string, " \
"symbol or module but got: #{name.inspect}"
end
ensure_block_has_source_location(block) { CallerFilter.first_non_rspec_line }
warn_if_key_taken context, name, block
metadata = Metadata.build_hash_from(metadata_args)
shared_module = SharedExampleGroupModule.new(name, block, metadata)
shared_example_groups[context][name] = shared_module
end
def find(lookup_contexts, name)
lookup_contexts.each do |context|
found = shared_example_groups[context][name]
return found if found
end
shared_example_groups[:main][name]
end
private
# TODO: remove this in RSpec 4. This exists only to support
# `config.shared_context_metadata_behavior == :trigger_inclusion`,
# the legacy behavior of shared context metadata, which we do
# not want to support in RSpec 4.
def legacy_add(context, name, *metadata_args, &block)
ensure_block_has_source_location(block) { CallerFilter.first_non_rspec_line }
shared_module = SharedExampleGroupModule.new(name, block, {})
if valid_name?(name)
warn_if_key_taken context, name, block
shared_example_groups[context][name] = shared_module
else
metadata_args.unshift name
end
return if metadata_args.empty?
RSpec.configuration.include shared_module, *metadata_args
end
def shared_example_groups
@shared_example_groups ||= Hash.new { |hash, context| hash[context] = {} }
end
def valid_name?(candidate)
case candidate
when String, Symbol, Module then true
else false
end
end
def warn_if_key_taken(context, key, new_block)
existing_module = shared_example_groups[context][key]
return unless existing_module
old_definition_location = formatted_location existing_module.definition
new_definition_location = formatted_location new_block
loaded_spec_files = RSpec.configuration.loaded_spec_files
if loaded_spec_files.include?(new_definition_location) && old_definition_location == new_definition_location
RSpec.warn_with <<-WARNING.gsub(/^ +\|/, ''), :call_site => nil
|WARNING: Your shared example group, '#{key}', defined at:
| #{old_definition_location}
|was automatically loaded by RSpec because the file name
|matches the configured autoloading pattern (#{RSpec.configuration.pattern}),
|and is also being required from somewhere else. To fix this
|warning, either rename the file to not match the pattern, or
|do not explicitly require the file.
WARNING
else
RSpec.warn_with <<-WARNING.gsub(/^ +\|/, ''), :call_site => nil
|WARNING: Shared example group '#{key}' has been previously defined at:
| #{old_definition_location}
|...and you are now defining it at:
| #{new_definition_location}
|The new definition will overwrite the original one.
WARNING
end
end
if RUBY_VERSION.to_f >= 1.9
def formatted_location(block)
block.source_location.join(":")
end
else # 1.8.7
# :nocov:
def formatted_location(block)
block.source_location.join(":").gsub(/:in.*$/, '')
end
# :nocov:
end
if Proc.method_defined?(:source_location)
def ensure_block_has_source_location(_block); end
else # for 1.8.7
# :nocov:
def ensure_block_has_source_location(block)
source_location = yield.split(':')
block.extend(Module.new { define_method(:source_location) { source_location } })
end
# :nocov:
end
end
end
end
instance_exec(&Core::SharedExampleGroup::TopLevelDSL.definitions)
end