mirror of
https://gitea.invidious.io/iv-org/shard-spectator.git
synced 2024-08-15 00:53:35 +00:00
Use Mocks DSL
This commit is contained in:
parent
e5fb4de4ae
commit
48a8408930
1 changed files with 1 additions and 459 deletions
|
@ -3,465 +3,7 @@ require "mocks/dsl/allow_syntax"
|
||||||
module Spectator::DSL
|
module Spectator::DSL
|
||||||
# Methods and macros for mocks and doubles.
|
# Methods and macros for mocks and doubles.
|
||||||
module Mocks
|
module Mocks
|
||||||
|
include ::Mocks::DSL::Methods
|
||||||
include ::Mocks::DSL::AllowSyntax
|
include ::Mocks::DSL::AllowSyntax
|
||||||
|
|
||||||
# All defined double and mock types.
|
|
||||||
# Each tuple consists of the double name or mocked type,
|
|
||||||
# defined context (example group), and double type name relative to its context.
|
|
||||||
TYPES = [] of {Symbol, Symbol, Symbol}
|
|
||||||
|
|
||||||
# Defines a new double type.
|
|
||||||
#
|
|
||||||
# This must be called from outside of a method (where classes can be defined).
|
|
||||||
# The *name* is the identifier used to reference the double, like when instantiating it.
|
|
||||||
# Simple stubbed methods returning a value can be defined by *value_methods*.
|
|
||||||
# More complex methods and stubs can be defined in a block passed to this macro.
|
|
||||||
#
|
|
||||||
# ```
|
|
||||||
# def_double(:dbl, foo: 42, bar: "baz") do
|
|
||||||
# stub abstract def deferred : String
|
|
||||||
# end
|
|
||||||
# ```
|
|
||||||
private macro def_double(name, **value_methods, &block)
|
|
||||||
{% # Construct a unique type name for the double by using the number of defined doubles.
|
|
||||||
index = ::Spectator::DSL::Mocks::TYPES.size
|
|
||||||
double_type_name = "Double#{index}".id
|
|
||||||
null_double_type_name = "NullDouble#{index}".id
|
|
||||||
|
|
||||||
# Store information about how the double is defined and its context.
|
|
||||||
# This is important for constructing an instance of the double later.
|
|
||||||
::Spectator::DSL::Mocks::TYPES << {name.id.symbolize, @type.name(generic_args: false).symbolize, double_type_name.symbolize} %}
|
|
||||||
|
|
||||||
# Define the plain double type.
|
|
||||||
::Mocks::Double.define({{double_type_name}}, {{**value_methods}}) do
|
|
||||||
{{block.body if block}}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Instantiates a double.
|
|
||||||
#
|
|
||||||
# The *name* is an optional identifier for the double.
|
|
||||||
# If *name* was previously used to define a double (with `#def_double`),
|
|
||||||
# then this macro returns a new instance of that previously defined double type.
|
|
||||||
# Otherwise, a `LazyDouble` is created and returned.
|
|
||||||
#
|
|
||||||
# Initial stubbed values for methods can be provided with *value_methods*.
|
|
||||||
#
|
|
||||||
# ```
|
|
||||||
# def_double(:dbl, foo: 42)
|
|
||||||
#
|
|
||||||
# specify do
|
|
||||||
# dbl = new_double(:dbl, foo: 7)
|
|
||||||
# expect(dbl.foo).to eq(7)
|
|
||||||
# lazy = new_double(:lazy, foo: 123)
|
|
||||||
# expect(lazy.foo).to eq(123)
|
|
||||||
# end
|
|
||||||
# ```
|
|
||||||
private macro new_double(name = nil, **value_methods)
|
|
||||||
{% # Find tuples with the same name.
|
|
||||||
found_tuples = ::Spectator::DSL::Mocks::TYPES.select { |tuple| tuple[0] == name.id.symbolize }
|
|
||||||
|
|
||||||
# Split the current context's type namespace into parts.
|
|
||||||
type_parts = @type.name(generic_args: false).split("::")
|
|
||||||
|
|
||||||
# Find tuples in the same context or a parent of where the double was defined.
|
|
||||||
# This is done by comparing each part of their namespaces.
|
|
||||||
found_tuples = found_tuples.select do |tuple|
|
|
||||||
# Split the namespace of the context the double was defined in.
|
|
||||||
context_parts = tuple[1].id.split("::")
|
|
||||||
|
|
||||||
# Compare namespace parts between the context the double was defined in and this context.
|
|
||||||
# This logic below is effectively comparing array elements, but with methods supported by macros.
|
|
||||||
matches = context_parts.map_with_index { |part, i| part == type_parts[i] }
|
|
||||||
matches.all? { |b| b }
|
|
||||||
end
|
|
||||||
|
|
||||||
# Sort the results by the number of namespace parts.
|
|
||||||
# The last result will be the double type defined closest to the current context's type.
|
|
||||||
found_tuples = found_tuples.sort_by do |tuple|
|
|
||||||
tuple[1].id.split("::").size
|
|
||||||
end
|
|
||||||
found_tuple = found_tuples.last %}
|
|
||||||
|
|
||||||
begin
|
|
||||||
%double = {% if found_tuple %}
|
|
||||||
{{found_tuple[2].id}}.new({{found_tuple[0].id.stringify}}, {{**value_methods}})
|
|
||||||
{% else %}
|
|
||||||
::Spectator::LazyDouble.new({{name}}, {{**value_methods}})
|
|
||||||
{% end %}
|
|
||||||
::Spectator::Harness.current?.try(&.cleanup { %double.__mocks.reset })
|
|
||||||
%double
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Instantiates a class double.
|
|
||||||
#
|
|
||||||
# The *name* is an optional identifier for the double.
|
|
||||||
# If *name* was previously used to define a double (with `#def_double`),
|
|
||||||
# then this macro returns a previously defined double class.
|
|
||||||
# Otherwise, `LazyDouble` is created and returned.
|
|
||||||
#
|
|
||||||
# Initial stubbed values for methods can be provided with *value_methods*.
|
|
||||||
#
|
|
||||||
# ```
|
|
||||||
# def_double(:dbl) do
|
|
||||||
# stub def self.foo
|
|
||||||
# 42
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# specify do
|
|
||||||
# dbl = class_double(:dbl)
|
|
||||||
# expect(dbl.foo).to eq(42)
|
|
||||||
# allow(dbl).to receive(:foo).and_return(123)
|
|
||||||
# expect(dbl.foo).to eq(123)
|
|
||||||
# end
|
|
||||||
# ```
|
|
||||||
private macro class_double(name = nil, **value_methods)
|
|
||||||
{% # Find tuples with the same name.
|
|
||||||
found_tuples = ::Spectator::DSL::Mocks::TYPES.select { |tuple| tuple[0] == name.id.symbolize }
|
|
||||||
|
|
||||||
# Split the current context's type namespace into parts.
|
|
||||||
type_parts = @type.name(generic_args: false).split("::")
|
|
||||||
|
|
||||||
# Find tuples in the same context or a parent of where the double was defined.
|
|
||||||
# This is done by comparing each part of their namespaces.
|
|
||||||
found_tuples = found_tuples.select do |tuple|
|
|
||||||
# Split the namespace of the context the double was defined in.
|
|
||||||
context_parts = tuple[1].id.split("::")
|
|
||||||
|
|
||||||
# Compare namespace parts between the context the double was defined in and this context.
|
|
||||||
# This logic below is effectively comparing array elements, but with methods supported by macros.
|
|
||||||
matches = context_parts.map_with_index { |part, i| part == type_parts[i] }
|
|
||||||
matches.all? { |b| b }
|
|
||||||
end
|
|
||||||
|
|
||||||
# Sort the results by the number of namespace parts.
|
|
||||||
# The last result will be the double type defined closest to the current context's type.
|
|
||||||
found_tuples = found_tuples.sort_by do |tuple|
|
|
||||||
tuple[1].id.split("::").size
|
|
||||||
end
|
|
||||||
found_tuple = found_tuples.last %}
|
|
||||||
|
|
||||||
begin
|
|
||||||
%double = {% if found_tuple %}
|
|
||||||
{{found_tuple[2].id}}
|
|
||||||
{% else %}
|
|
||||||
::Spectator::LazyDouble
|
|
||||||
{% end %}
|
|
||||||
{% for key, value in value_methods %}
|
|
||||||
%stub{key} = ::Spectator::ValueStub.new({{key.id.symbolize}}, {{value}})
|
|
||||||
%double._spectator_define_stub(%stub{key})
|
|
||||||
{% end %}
|
|
||||||
::Spectator::Harness.current?.try(&.cleanup { %double.__mocks.reset })
|
|
||||||
%double
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Defines or instantiates a double.
|
|
||||||
#
|
|
||||||
# When used inside of a method, instantiates a new double.
|
|
||||||
# See `#new_double`.
|
|
||||||
#
|
|
||||||
# When used outside of a method, defines a new double.
|
|
||||||
# See `#def_double`.
|
|
||||||
macro double(name, **value_methods, &block)
|
|
||||||
{% begin %}
|
|
||||||
{% if @def %}new_double{% else %}def_double{% end %}({{name}}, {{**value_methods}}) {{block}}
|
|
||||||
{% end %}
|
|
||||||
end
|
|
||||||
|
|
||||||
# Instantiates a new double with predefined responses.
|
|
||||||
#
|
|
||||||
# This constructs a `LazyDouble`.
|
|
||||||
#
|
|
||||||
# ```
|
|
||||||
# dbl = double(foo: 42)
|
|
||||||
# expect(dbl.foo).to eq(42)
|
|
||||||
# ```
|
|
||||||
macro double(**value_methods)
|
|
||||||
::Spectator::LazyDouble.new({{**value_methods}})
|
|
||||||
end
|
|
||||||
|
|
||||||
# Defines a new mock type.
|
|
||||||
#
|
|
||||||
# This must be called from outside of a method (where classes can be defined).
|
|
||||||
# *type* is the type being mocked.
|
|
||||||
# The *name* is an optional identifier used in debug output.
|
|
||||||
# Simple stubbed methods returning a value can be defined by *value_methods*.
|
|
||||||
# More complex methods and stubs can be defined in a block passed to this macro.
|
|
||||||
#
|
|
||||||
# ```
|
|
||||||
# abstract class MyClass
|
|
||||||
# def foo
|
|
||||||
# 42
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# def bar
|
|
||||||
# Time.utc
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# def_mock(MyClass, foo: 5) do
|
|
||||||
# stub def bar
|
|
||||||
# Time.utc(2022, 7, 10)
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
# ```
|
|
||||||
private macro def_mock(type, name = nil, **value_methods, &block)
|
|
||||||
{% resolved = type.resolve
|
|
||||||
# Construct a unique type name for the mock by using the number of defined types.
|
|
||||||
index = ::Spectator::DSL::Mocks::TYPES.size
|
|
||||||
# The type is nested under the original so that any type names from the original can be resolved.
|
|
||||||
mock_type_name = "Mock#{index}".id
|
|
||||||
|
|
||||||
# Store information about how the mock is defined and its context.
|
|
||||||
# This is important for constructing an instance of the mock later.
|
|
||||||
::Spectator::DSL::Mocks::TYPES << {type.id.symbolize, @type.name(generic_args: false).symbolize, "::#{resolved.name}::#{mock_type_name}".id.symbolize}
|
|
||||||
|
|
||||||
base = if resolved.class?
|
|
||||||
:class
|
|
||||||
elsif resolved.struct?
|
|
||||||
:struct
|
|
||||||
else
|
|
||||||
:module
|
|
||||||
end %}
|
|
||||||
|
|
||||||
{% begin %}
|
|
||||||
{{base.id}} ::{{resolved.name}}
|
|
||||||
::Mocks::Mock.define({{mock_type_name}} < ::{{resolved.name}}, {{**value_methods}}) {{block}}
|
|
||||||
end
|
|
||||||
{% end %}
|
|
||||||
end
|
|
||||||
|
|
||||||
# Instantiates a mock.
|
|
||||||
#
|
|
||||||
# *type* is the type being mocked.
|
|
||||||
#
|
|
||||||
# Initial stubbed values for methods can be provided with *value_methods*.
|
|
||||||
#
|
|
||||||
# ```
|
|
||||||
# abstract class MyClass
|
|
||||||
# def foo
|
|
||||||
# 42
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# def bar
|
|
||||||
# Time.utc
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# def_mock(MyClass, foo: 5) do
|
|
||||||
# stub def bar
|
|
||||||
# Time.utc(2022, 7, 10)
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# specify do
|
|
||||||
# dbl = new_mock(MyClass, foo: 7)
|
|
||||||
# expect(dbl.foo).to eq(7)
|
|
||||||
# expect(dbl.bar).to eq(Time.utc(2022, 7, 10))
|
|
||||||
# end
|
|
||||||
# ```
|
|
||||||
private macro new_mock(type, **value_methods)
|
|
||||||
{% # Find tuples with the same name.
|
|
||||||
found_tuples = ::Spectator::DSL::Mocks::TYPES.select { |tuple| tuple[0] == type.id.symbolize }
|
|
||||||
|
|
||||||
# Split the current context's type namespace into parts.
|
|
||||||
type_parts = @type.name(generic_args: false).split("::")
|
|
||||||
|
|
||||||
# Find tuples in the same context or a parent of where the mock was defined.
|
|
||||||
# This is done by comparing each part of their namespaces.
|
|
||||||
found_tuples = found_tuples.select do |tuple|
|
|
||||||
# Split the namespace of the context the double was defined in.
|
|
||||||
context_parts = tuple[1].id.split("::")
|
|
||||||
|
|
||||||
# Compare namespace parts between the context the double was defined in and this context.
|
|
||||||
# This logic below is effectively comparing array elements, but with methods supported by macros.
|
|
||||||
matches = context_parts.map_with_index { |part, i| part == type_parts[i] }
|
|
||||||
matches.all? { |b| b }
|
|
||||||
end
|
|
||||||
|
|
||||||
# Sort the results by the number of namespace parts.
|
|
||||||
# The last result will be the double type defined closest to the current context's type.
|
|
||||||
found_tuples = found_tuples.sort_by do |tuple|
|
|
||||||
tuple[1].id.split("::").size
|
|
||||||
end
|
|
||||||
found_tuple = found_tuples.last %}
|
|
||||||
|
|
||||||
{% if found_tuple %}
|
|
||||||
{{found_tuple[2].id}}.new.tap do |%mock|
|
|
||||||
{% for key, value in value_methods %}
|
|
||||||
%stub{key} = ::Mocks::ValueStub.new({{key.id.symbolize}}, {{value}})
|
|
||||||
%mock.__mocks.add_stub(%stub{key})
|
|
||||||
{% end %}
|
|
||||||
::Spectator::Harness.current?.try(&.cleanup { %mock.__mocks.reset })
|
|
||||||
end
|
|
||||||
{% else %}
|
|
||||||
{% raise "Type `#{type.id}` must be previously mocked before attempting to instantiate." %}
|
|
||||||
{% end %}
|
|
||||||
end
|
|
||||||
|
|
||||||
# Defines or instantiates a mock.
|
|
||||||
#
|
|
||||||
# When used inside of a method, instantiates a new mock.
|
|
||||||
# See `#new_mock`.
|
|
||||||
#
|
|
||||||
# When used outside of a method, defines a new mock.
|
|
||||||
# See `#def_mock`.
|
|
||||||
macro mock(type, **value_methods, &block)
|
|
||||||
{% raise "First argument of `mock` must be a type name, not #{type}" unless type.is_a?(Path) || type.is_a?(Generic) || type.is_a?(Union) || type.is_a?(Metaclass) || type.is_a?(TypeNode) %}
|
|
||||||
{% begin %}
|
|
||||||
{% if @def %}new_mock{% else %}def_mock{% end %}({{type}}, {{**value_methods}}) {{block}}
|
|
||||||
{% end %}
|
|
||||||
end
|
|
||||||
|
|
||||||
# Instantiates a class mock.
|
|
||||||
#
|
|
||||||
# *type* is the type being mocked.
|
|
||||||
#
|
|
||||||
# Initial stubbed values for methods can be provided with *value_methods*.
|
|
||||||
#
|
|
||||||
# ```
|
|
||||||
# class MyClass
|
|
||||||
# def self.foo
|
|
||||||
# 42
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# def_mock(MyClass)
|
|
||||||
#
|
|
||||||
# specify do
|
|
||||||
# mock = class_mock(MyClass, foo: 5)
|
|
||||||
# expect(dbl.foo).to eq(5)
|
|
||||||
# allow(dbl).to receive(:foo).and_return(123)
|
|
||||||
# expect(dbl.foo).to eq(123)
|
|
||||||
# end
|
|
||||||
# ```
|
|
||||||
private macro class_mock(type, **value_methods)
|
|
||||||
{% # Find tuples with the same name.
|
|
||||||
found_tuples = ::Spectator::DSL::Mocks::TYPES.select { |tuple| tuple[0] == type.id.symbolize }
|
|
||||||
|
|
||||||
# Split the current context's type namespace into parts.
|
|
||||||
type_parts = @type.name(generic_args: false).split("::")
|
|
||||||
|
|
||||||
# Find tuples in the same context or a parent of where the mock was defined.
|
|
||||||
# This is done by comparing each part of their namespaces.
|
|
||||||
found_tuples = found_tuples.select do |tuple|
|
|
||||||
# Split the namespace of the context the double was defined in.
|
|
||||||
context_parts = tuple[1].id.split("::")
|
|
||||||
|
|
||||||
# Compare namespace parts between the context the double was defined in and this context.
|
|
||||||
# This logic below is effectively comparing array elements, but with methods supported by macros.
|
|
||||||
matches = context_parts.map_with_index { |part, i| part == type_parts[i] }
|
|
||||||
matches.all? { |b| b }
|
|
||||||
end
|
|
||||||
|
|
||||||
# Sort the results by the number of namespace parts.
|
|
||||||
# The last result will be the double type defined closest to the current context's type.
|
|
||||||
found_tuples = found_tuples.sort_by do |tuple|
|
|
||||||
tuple[1].id.split("::").size
|
|
||||||
end
|
|
||||||
found_tuple = found_tuples.last %}
|
|
||||||
|
|
||||||
{% if found_tuple %}
|
|
||||||
begin
|
|
||||||
%mock = {{found_tuple[2].id}}
|
|
||||||
{% for key, value in value_methods %}
|
|
||||||
%stub{key} = ::Mocks::ValueStub.new({{key.id.symbolize}}, {{value}})
|
|
||||||
%mock.__mocks.add_stub(%stub{key})
|
|
||||||
{% end %}
|
|
||||||
::Spectator::Harness.current?.try(&.cleanup { %mock._spectator_reset })
|
|
||||||
%mock
|
|
||||||
end
|
|
||||||
{% else %}
|
|
||||||
{% raise "Type `#{type.id}` must be previously mocked before attempting to instantiate." %}
|
|
||||||
{% end %}
|
|
||||||
end
|
|
||||||
|
|
||||||
# Injects mock (stub) functionality into an existing type.
|
|
||||||
#
|
|
||||||
# Warning: Using this will modify the type being tested.
|
|
||||||
# This may result in different behavior between test and non-test code.
|
|
||||||
#
|
|
||||||
# This must be used instead of `def_mock` if a concrete struct is tested.
|
|
||||||
# The `mock` method is not necessary to create a type with an injected mock.
|
|
||||||
# The type can be used as it would normally instead.
|
|
||||||
# However, stub information may leak between examples.
|
|
||||||
#
|
|
||||||
# The *type* is the name of the type to inject mock functionality into.
|
|
||||||
# Initial stubbed values for methods can be provided with *value_methods*.
|
|
||||||
#
|
|
||||||
# ```
|
|
||||||
# struct MyStruct
|
|
||||||
# def foo
|
|
||||||
# 42
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# inject_mock(MyStruct, foo: 5)
|
|
||||||
#
|
|
||||||
# specify do
|
|
||||||
# inst = MyStruct.new
|
|
||||||
# expect(inst.foo).to eq(5)
|
|
||||||
# allow(inst).to receive(:foo).and_return(123)
|
|
||||||
# expect(inst.foo).to eq(123)
|
|
||||||
# end
|
|
||||||
# ```
|
|
||||||
macro inject_mock(type, **value_methods, &block)
|
|
||||||
{% resolved = type.resolve
|
|
||||||
base = if resolved.class?
|
|
||||||
:class
|
|
||||||
elsif resolved.struct?
|
|
||||||
:struct
|
|
||||||
else
|
|
||||||
:module
|
|
||||||
end
|
|
||||||
|
|
||||||
# Store information about how the mock is defined and its context.
|
|
||||||
# This isn't required, but new_mock() should still find this type.
|
|
||||||
::Spectator::DSL::Mocks::TYPES << {type.id.symbolize, @type.name(generic_args: false).symbolize, resolved.name.symbolize} %}
|
|
||||||
|
|
||||||
{% begin %}
|
|
||||||
{{base.id}} {{type.id}}
|
|
||||||
include ::Mocks::Stubbable::Automatic
|
|
||||||
|
|
||||||
{% for key, value in value_methods %}
|
|
||||||
stub_any_args {{key}} = {{value}}
|
|
||||||
{% end %}
|
|
||||||
|
|
||||||
{{block.body if block}}
|
|
||||||
end
|
|
||||||
{% end %}
|
|
||||||
end
|
|
||||||
|
|
||||||
# Constructs a stub for a method.
|
|
||||||
#
|
|
||||||
# The *method* is the name of the method to stub.
|
|
||||||
#
|
|
||||||
# This is also the start of a fluent interface for defining stubs.
|
|
||||||
#
|
|
||||||
# Allow syntax:
|
|
||||||
# ```
|
|
||||||
# allow(dbl).to receive(:some_method)
|
|
||||||
# allow(dbl).to receive(:the_answer).and_return(42)
|
|
||||||
# ```
|
|
||||||
macro receive(method, *, _file = __FILE__, _line = __LINE__, &block)
|
|
||||||
{% if block %}
|
|
||||||
::Mocks::ProcStub.new({{method.id.symbolize}}) {{block}}
|
|
||||||
{% else %}
|
|
||||||
::Mocks::NilStub.new({{method.id.symbolize}})
|
|
||||||
{% end %}
|
|
||||||
end
|
|
||||||
|
|
||||||
# Returns empty arguments.
|
|
||||||
def no_args
|
|
||||||
::Mocks::Arguments.none
|
|
||||||
end
|
|
||||||
|
|
||||||
# Indicates any arguments can be used (no constraint).
|
|
||||||
def any_args
|
|
||||||
::Mocks::Arguments.any
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue