Add lazy double

This commit is contained in:
Michael Miller 2022-03-19 19:32:41 -06:00
parent 162ad4df33
commit bed84b315d
No known key found for this signature in database
GPG key ID: 32B47AE8F388A1FF
4 changed files with 397 additions and 13 deletions

View file

@ -33,7 +33,7 @@ module Spectator::DSL
{% end %}
end
private macro new_double(name)
private macro new_double(name = nil, **value_methods)
{% # Find tuples with the same name.
found_tuples = ::Spectator::DSL::Mocks::DOUBLES.select { |tuple| tuple[0] == name.id.symbolize }
@ -57,25 +57,25 @@ module Spectator::DSL
found_tuples = found_tuples.sort_by do |tuple|
tuple[1].id.split("::").size
end
found_tuple = found_tuples.last
raise "Undefined double type '#{name}'" unless found_tuple
found_tuple = found_tuples.last %}
# Store the type name used to define the underlying double.
double_type_name = found_tuple[2].id %}
{{double_type_name}}.new
{% if found_tuple %}
{{found_tuple[2].id}}.new
{% else %}
::Spectator::LazyDouble.new({{name}}, {{**value_methods}})
{% end %}
end
macro double(name, **value_methods, &block)
{% if @def %}
new_double({{name}}){% if block %} do
{{block.body}}
end{% end %}
{% else %}
def_double({{name}}, {{**value_methods}}){% if block %} do
{% begin %}
{% if @def %}new_double{% else %}def_double{% end %}({{name}}, {{**value_methods}}){% if block %} do
{{block.body}}
end{% end %}
{% end %}
end
macro double(**value_methods)
new_double({{**value_methods}})
end
end
end

View file

@ -0,0 +1,60 @@
require "../label"
require "./arguments"
require "./double"
require "./method_call"
require "./stub"
module Spectator
# Stands in for an object for testing that a SUT calls expected methods.
#
# Handles all messages (method calls), but only responds to those configured.
# Methods called that were not configured will raise `UnexpectedMessage`.
#
# Use `#_spectator_define_stub` to override behavior of a method in the double.
# Only methods defined in the double's type can have stubs.
# New methods are not defines when a stub is added that doesn't have a matching method name.
class LazyDouble(Messages) < Double
@name : String?
def initialize(_spectator_double_name = nil, _spectator_double_stubs = [] of Stub, **@messages : **Messages)
super(_spectator_double_stubs)
@name = _spectator_double_name.try &.inspect
end
# Returns the double's name formatted for user output.
private def _spectator_stubbed_name : String
"#<LazyDouble #{@name || "Anonymous"}>"
end
# Handles all messages.
macro method_missing(call)
# Capture information about the call.
%args = ::Spectator::Arguments.capture({{call.args.splat(", ")}}{% if call.named_args %}{{*call.named_args}}{% end %})
%call = ::Spectator::MethodCall.new({{call.name.symbolize}}, %args)
# Attempt to find a stub that satisfies the method call and arguments.
if %stub = _spectator_find_stub(%call)
# Cast the stub or return value to the expected type.
# This is necessary to match the expected return type of the original message.
\{% if Messages.keys.includes?({{call.name.symbolize}}) %}
_spectator_cast_stub_value(%stub, %call, \{{Messages[{{call.name.symbolize}}.id]}}, \{{Messages[{{call.name.symbolize}}.id].resolve >= Nil}})
\{% else %}
# A method that was not defined during initialization was stubbed.
# Return the value of the stub as-is.
# Might want to give a warning here, as this may produce a "bloated" union of all known stub types.
%stub.value
\{% end %}
else
# A stub wasn't found, invoke the fallback logic.
\{% if Messages.keys.includes?({{call.name.symbolize}}.id) %}
# Pass along the message type and a block to invoke it.
_spectator_stub_fallback(%call, \{{Messages[{{call.name.symbolize}}.id]}}) { @messages[{{call.name.symbolize}}] }
\{% else %}
# Message received for a methods that isn't stubbed nor defined when initialized.
_spectator_abstract_stub_fallback(%call)
nil # Necessary for compiler to infer return type as nil. Avoids runtime "can't execute ... `x` has no type errors".
\{% end %}
end
end
end
end