2022-03-05 17:41:52 +00:00
require " ../mocks "
module Spectator::DSL
# Methods and macros for mocks and doubles.
module Mocks
2022-04-02 23:58:15 +00:00
# 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 }
2022-03-12 16:39:32 +00:00
2022-04-01 00:51:28 +00:00
# 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
# ```
2022-03-19 17:41:53 +00:00
private macro def_double ( name , ** value_methods , & block )
2022-07-08 01:09:18 +00:00
{% # Construct a unique type name for the double by using the number of defined doubles.
2022-04-02 23:58:15 +00:00
index = :: Spectator :: DSL :: Mocks :: TYPES . size
2022-03-19 20:41:45 +00:00
double_type_name = " Double #{ index } " . id
null_double_type_name = " NullDouble #{ index } " . id
2022-03-12 16:39:32 +00:00
2022-03-19 17:41:53 +00:00
# Store information about how the double is defined and its context.
# This is important for constructing an instance of the double later.
2022-04-02 23:58:15 +00:00
:: Spectator :: DSL :: Mocks :: TYPES << { name . id . symbolize , @type . name ( generic_args : false ) . symbolize , double_type_name . symbolize } % }
2022-03-19 20:41:45 +00:00
2022-04-01 00:51:28 +00:00
# Define the plain double type.
2022-03-19 20:41:45 +00:00
:: Spectator :: Double . define ( {{ double_type_name }} , {{ name }} , {{ ** value_methods }} ) do
2022-04-01 00:51:28 +00:00
# Returns a new double that responds to undefined methods with itself.
# See: `NullDouble`
2022-03-19 20:41:45 +00:00
def as_null_object
{{ null_double_type_name }} . new ( @stubs )
end
2022-10-09 22:04:07 +00:00
{{ block . body if block }}
2022-03-19 20:41:45 +00:00
end
2022-03-19 17:41:53 +00:00
{% begin %}
2022-04-01 00:51:28 +00:00
# Define a matching null double type.
2022-07-13 03:46:12 +00:00
:: Spectator :: NullDouble . define ( {{ null_double_type_name }} , {{ name }} , {{ ** value_methods }} ) {{ block }}
2022-03-19 17:41:53 +00:00
{% end %}
end
2022-04-01 00:51:28 +00:00
# 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
# ```
2022-03-20 01:32:41 +00:00
private macro new_double ( name = nil , ** value_methods )
2022-03-19 17:41:53 +00:00
{% # Find tuples with the same name.
2022-04-02 23:58:15 +00:00
found_tuples = :: Spectator :: DSL :: Mocks :: TYPES . select { | tuple | tuple [ 0 ] == name . id . symbolize }
2022-03-12 16:39:32 +00:00
# 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
2022-03-20 01:32:41 +00:00
found_tuple = found_tuples . last % }
2022-03-12 16:39:32 +00:00
2022-07-13 02:40:27 +00:00
begin
% double = {% if found_tuple %}
{{ found_tuple [ 2 ] . id }} . new ( {{ ** value_methods }} )
{% else %}
:: Spectator :: LazyDouble . new ( {{ name }} , {{ ** value_methods }} )
{% end %}
:: Spectator :: Harness . current? . try ( & . cleanup { % double . _spectator_reset } )
% double
end
2022-03-19 17:41:53 +00:00
end
2022-03-12 16:39:32 +00:00
2022-07-11 00:33:56 +00:00
# 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.
#
2022-07-14 19:18:02 +00:00
# Initial stubbed values for methods can be provided with *value_methods*.
#
2022-07-11 00:33:56 +00:00
# ```
# 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
# ```
2022-07-14 19:18:02 +00:00
private macro class_double ( name = nil , ** value_methods )
2022-07-08 01:09:18 +00:00
{% # 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 % }
2022-07-13 02:40:27 +00:00
begin
% double = {% if found_tuple %}
{{ found_tuple [ 2 ] . id }}
{% else %}
:: Spectator :: LazyDouble
{% end %}
2022-07-14 19:18:02 +00:00
{% for key , value in value_methods %}
% stub { key } = :: Spectator :: ValueStub . new ( {{ key . id . symbolize }} , {{ value }} )
% double . _spectator_define_stub ( % stub { key } )
{% end %}
2022-07-13 02:40:27 +00:00
:: Spectator :: Harness . current? . try ( & . cleanup { % double . _spectator_reset } )
% double
end
2022-07-08 01:09:18 +00:00
end
2022-04-01 00:51:28 +00:00
# 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`.
2022-03-19 17:41:53 +00:00
macro double ( name , ** value_methods , & block )
2022-03-20 01:32:41 +00:00
{% begin %}
2022-07-13 03:46:12 +00:00
{% if @def %} new_double {% else %} def_double {% end %} ( {{ name }} , {{ ** value_methods }} ) {{ block }}
2022-03-12 16:39:32 +00:00
{% end %}
end
2022-03-20 01:32:41 +00:00
2022-04-01 00:51:28 +00:00
# Instantiates a new double with predefined responses.
#
# This constructs a `LazyDouble`.
#
# ```
# dbl = double(foo: 42)
# expect(dbl.foo).to eq(42)
# ```
2022-03-20 01:32:41 +00:00
macro double ( ** value_methods )
2022-04-01 00:54:39 +00:00
:: Spectator :: LazyDouble . new ( {{ ** value_methods }} )
2022-03-20 01:32:41 +00:00
end
2022-04-01 04:49:45 +00:00
2022-07-11 00:33:56 +00:00
# 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
# ```
2022-05-28 15:18:49 +00:00
private macro def_mock ( type , name = nil , ** value_methods , & block )
2022-12-18 03:56:16 +00:00
{% 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
2022-04-03 00:43:13 +00:00
2022-12-18 03:56:16 +00:00
# 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 }
2022-05-28 15:18:49 +00:00
2022-12-18 03:56:16 +00:00
base = if resolved . class?
:class
elsif resolved . struct?
:struct
else
:module
end % }
2022-05-28 15:18:49 +00:00
2022-12-18 03:56:16 +00:00
{% begin %}
{{ base . id }} :: {{ resolved . name }}
:: Spectator :: Mock . define_subtype ( {{ base }} , {{ type . id }} , {{ mock_type_name }} , {{ name }} , {{ ** value_methods }} ) {{ block }}
end
{% end %}
2022-04-03 00:43:13 +00:00
end
2022-07-11 00:33:56 +00:00
# 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
# ```
2022-04-03 00:43:13 +00:00
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 %}
2022-07-13 03:19:51 +00:00
{{ found_tuple [ 2 ] . id }} . new . tap do | % mock |
2022-06-02 04:04:18 +00:00
{% for key , value in value_methods %}
% stub { key } = :: Spectator :: ValueStub . new ( {{ key . id . symbolize }} , {{ value }} )
2022-07-13 01:50:04 +00:00
% mock . _spectator_define_stub ( % stub { key } )
2022-06-02 04:04:18 +00:00
{% end %}
2022-07-13 02:40:27 +00:00
:: Spectator :: Harness . current? . try ( & . cleanup { % mock . _spectator_reset } )
2022-06-02 04:04:18 +00:00
end
2022-04-03 00:43:13 +00:00
{% else %}
{% raise " Type ` #{ type . id } ` must be previously mocked before attempting to instantiate. " %}
{% end %}
end
2022-07-11 00:33:56 +00:00
# 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`.
2022-04-03 00:43:13 +00:00
macro mock ( type , ** value_methods , & block )
2022-06-02 04:23:39 +00:00
{% 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 ) %}
2022-04-03 00:43:13 +00:00
{% begin %}
2022-07-13 03:46:12 +00:00
{% if @def %} new_mock {% else %} def_mock {% end %} ( {{ type }} , {{ ** value_methods }} ) {{ block }}
2022-04-03 00:43:13 +00:00
{% end %}
end
2022-07-11 00:33:56 +00:00
# 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
# ```
2022-07-08 01:09:18 +00:00
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 %}
2022-07-13 02:40:27 +00:00
begin
% mock = {{ found_tuple [ 2 ] . id }}
2022-07-08 01:09:18 +00:00
{% for key , value in value_methods %}
% stub { key } = :: Spectator :: ValueStub . new ( {{ key . id . symbolize }} , {{ value }} )
2022-07-13 01:50:04 +00:00
% mock . _spectator_define_stub ( % stub { key } )
2022-07-08 01:09:18 +00:00
{% end %}
2022-07-13 02:40:27 +00:00
:: Spectator :: Harness . current? . try ( & . cleanup { % mock . _spectator_reset } )
% mock
2022-07-08 01:09:18 +00:00
end
{% else %}
{% raise " Type ` #{ type . id } ` must be previously mocked before attempting to instantiate. " %}
{% end %}
end
2022-07-13 04:12:48 +00:00
# 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 } % }
2022-07-13 17:29:19 +00:00
:: Spectator :: Mock . inject ( {{ base }} , :: {{ resolved . name }} , {{ ** value_methods }} ) {{ block }}
2022-07-13 04:12:48 +00:00
end
2022-04-02 16:25:27 +00:00
# Targets a stubbable object (such as a mock or double) for operations.
#
2022-07-08 01:01:02 +00:00
# The *stubbable* must be a `Stubbable` or `StubbedType`.
2022-04-02 16:25:27 +00:00
# This method is expected to be followed up with `.to receive()`.
#
# ```
# dbl = dbl(:foobar)
# allow(dbl).to receive(:foo).and_return(42)
# ```
2022-07-08 01:01:02 +00:00
def allow ( stubbable : Stubbable | StubbedType )
2022-04-02 16:25:27 +00:00
:: Spectator :: Allow . new ( stubbable )
end
# Helper method producing a compilation error when attempting to stub a non-stubbable object.
#
# Triggered in cases like this:
# ```
# allow(42).to receive(:to_s).and_return("123")
# ```
def allow ( stubbable )
{% raise " Target of `allow()` must be stubbable (mock or double). " %}
2022-04-01 04:49:45 +00:00
end
2022-04-02 16:25:27 +00:00
# Begins the creation of a stub.
#
# The *method* is the name of the method being stubbed.
# It should not define any parameters, it should be just the method name as a literal symbol or string.
#
# Alone, this method returns a `NullStub`, which allows a stubbable object to return nil from a method.
# This macro is typically followed up with a method like `and_return` to change the stub's behavior.
#
# ```
# dbl = dbl(:foobar)
# allow(dbl).to receive(:foo)
# expect(dbl.foo).to be_nil
#
# allow(dbl).to receive(:foo).and_return(42)
# expect(dbl.foo).to eq(42)
# ```
2022-07-10 17:54:51 +00:00
#
# A block can be provided to be run every time the stub is invoked.
# The value returned by the block is returned by the stubbed method.
#
# ```
# dbl = dbl(:foobar)
# allow(dbl).to receive(:foo) { 42 }
# expect(dbl.foo).to eq(42)
# ```
macro receive ( method , * , _file = __FILE__ , _line = __LINE__ , & block )
{% if block %}
% proc = - > ( % args : :: Spectator :: AbstractArguments ) {
{% if ! block . args . empty? %} {{ * block . args }} = % args {% end %}
{{ block . body }}
}
:: Spectator :: ProcStub . new ( {{ method . id . symbolize }} , % proc , location : :: Spectator :: Location . new ( {{ _file }} , {{ _line }} ) )
{% else %}
:: Spectator :: NullStub . new ( {{ method . id . symbolize }} , location : :: Spectator :: Location . new ( {{ _file }} , {{ _line }} ) )
{% end %}
2022-04-01 04:49:45 +00:00
end
2022-07-11 00:51:58 +00:00
# Returns empty arguments.
def no_args
2022-07-13 00:59:23 +00:00
:: Spectator :: Arguments . none
2022-07-11 00:51:58 +00:00
end
2022-07-13 01:05:55 +00:00
# Indicates any arguments can be used (no constraint).
def any_args
:: Spectator :: Arguments . any
end
2022-03-05 17:41:52 +00:00
end
end