shard-spectator/src/spectator/mocks/mock.cr

204 lines
7.3 KiB
Crystal

require "./method_call"
require "./mocked"
require "./reference_mock_registry"
require "./stub"
require "./stubbed_name"
require "./stubbed_type"
require "./value_mock_registry"
require "./value_stub"
module Spectator
# Module providing macros for defining new mocks from existing types and injecting mock features into concrete types.
module Mock
# Defines a type that inherits from another, existing type.
# The newly defined subtype will have mocking functionality.
#
# Methods from the inherited type will be overridden to support stubs.
# *base* is the keyword for the type being defined - class or struct.
# *mocked_type* is the original type to inherit from.
# *type_name* is the name of the new type to define.
# An optional *name* of the mock can be provided.
# Any key-value pairs provided with *value_methods* are used as initial stubs for the mocked type.
#
# A block can be provided to define additional methods and stubs.
# The block is evaluated in the context of the derived type.
#
# ```
# Mock.define_subtype(:class, SomeType, meth1: 42, meth2: "foobar") do
# stub abstract def meth3 : Symbol
#
# # Default implementation with a dynamic value.
# stub def meth4
# Time.utc
# end
# end
# ```
macro define_subtype(base, mocked_type, type_name, name = nil, **value_methods, &block)
{% begin %}
{% if name %}@[::Spectator::StubbedName({{name}})]{% end %}
{% if base.id == :module.id %}
{{base.id}} {{type_name.id}}
include {{mocked_type.id}}
{% else %}
{{base.id}} {{type_name.id}} < {{mocked_type.id}}
{% end %}
include ::Spectator::Mocked
extend ::Spectator::StubbedType
{% begin %}
private getter(_spectator_stubs) do
[
{% for key, value in value_methods %}
::Spectator::ValueStub.new({{key.id.symbolize}}, {{value}}),
{% end %}
] of ::Spectator::Stub
end
{% end %}
def _spectator_remove_stub(stub : ::Spectator::Stub) : ::Nil
@_spectator_stubs.try &.delete(stub)
end
def _spectator_clear_stubs : ::Nil
@_spectator_stubs = nil
end
private class_getter _spectator_stubs : ::Array(::Spectator::Stub) = [] of ::Spectator::Stub
class_getter _spectator_calls : ::Array(::Spectator::MethodCall) = [] of ::Spectator::MethodCall
getter _spectator_calls = [] of ::Spectator::MethodCall
# Returns the mock's name formatted for user output.
private def _spectator_stubbed_name : ::String
\{% if anno = @type.annotation(::Spectator::StubbedName) %}
"#<Mock {{mocked_type.id}} \"" + \{{(anno[0] || :Anonymous.id).stringify}} + "\">"
\{% else %}
"#<Mock {{mocked_type.id}}>"
\{% end %}
end
private def self._spectator_stubbed_name : ::String
\{% if anno = @type.annotation(::Spectator::StubbedName) %}
"#<Class Mock {{mocked_type.id}} \"" + \{{(anno[0] || :Anonymous.id).stringify}} + "\">"
\{% else %}
"#<Class Mock {{mocked_type.id}}>"
\{% end %}
end
macro finished
stub_type {{mocked_type.id}}
{{block.body if block}}
end
end
{% end %}
end
# Injects mock functionality into an existing type.
#
# Generally this method of mocking should be avoiding.
# It modifies types being tested, the mock functionality won't exist outside of tests.
# This option should only be used when sub-types are not possible (e.g. concrete struct).
#
# Methods in the type will be overridden to support stubs.
# The original method functionality will still be accessible, but pass through mock code first.
# *base* is the keyword for the type being defined - class or struct.
# *type_name* is the name of the type to inject mock functionality into.
# This _must_ be full, resolvable path to the type.
# An optional *name* of the mock can be provided.
# Any key-value pairs provided with *value_methods* are used as initial stubs for the mocked type.
#
# A block can be provided to define additional methods and stubs.
# The block is evaluated in the context of the derived type.
#
# ```
# Mock.inject(:struct, SomeType, meth1: 42, meth2: "foobar") do
# stub abstract def meth3 : Symbol
#
# # Default implementation with a dynamic value.
# stub def meth4
# Time.utc
# end
# end
# ```
macro inject(base, type_name, name = nil, **value_methods, &block)
{% begin %}
{% if name %}@[::Spectator::StubbedName({{name}})]{% end %}
{{base.id}} ::{{type_name.id}}
include ::Spectator::Mocked
extend ::Spectator::StubbedType
{% if base == :class %}
@@_spectator_mock_registry = ::Spectator::ReferenceMockRegistry.new
{% elsif base == :struct %}
@@_spectator_mock_registry = ::Spectator::ValueMockRegistry(self).new
{% else %}
{% raise "Unsupported base type #{base} for injecting mock" %}
{% end %}
private class_getter _spectator_stubs : ::Array(::Spectator::Stub) = [] of ::Spectator::Stub
class_getter _spectator_calls : ::Array(::Spectator::MethodCall) = [] of ::Spectator::MethodCall
private def _spectator_stubs
entry = @@_spectator_mock_registry.fetch(self) do
_spectator_default_stubs
end
entry.stubs
end
def _spectator_remove_stub(stub : ::Spectator::Stub) : ::Nil
@@_spectator_mock_registry[self]?.try &.stubs.delete(stub)
end
def _spectator_clear_stubs : ::Nil
@@_spectator_mock_registry.delete(self)
end
def _spectator_calls
entry = @@_spectator_mock_registry.fetch(self) do
_spectator_default_stubs
end
entry.calls
end
private def _spectator_default_stubs
{% begin %}
[
{% for key, value in value_methods %}
::Spectator::ValueStub.new({{key.id.symbolize}}, {{value}}),
{% end %}
] of ::Spectator::Stub
{% end %}
end
# Returns the mock's name formatted for user output.
private def _spectator_stubbed_name : ::String
\{% if anno = @type.annotation(::Spectator::StubbedName) %}
"#<Mock {{type_name.id}} \"" + \{{(anno[0] || :Anonymous.id).stringify}} + "\">"
\{% else %}
"#<Mock {{type_name.id}}>"
\{% end %}
end
# Returns the mock's name formatted for user output.
private def self._spectator_stubbed_name : ::String
\{% if anno = @type.annotation(::Spectator::StubbedName) %}
"#<Class Mock {{type_name.id}} \"" + \{{(anno[0] || :Anonymous.id).stringify}} + "\">"
\{% else %}
"#<Class Mock {{type_name.id}}>"
\{% end %}
end
macro finished
stub_type {{type_name.id}}
{{block.body if block}}
end
end
{% end %}
end
end
end