Cleanup and docs

This commit is contained in:
Michael Miller 2022-03-12 15:43:12 -07:00
parent 151926fd25
commit 7931847164
No known key found for this signature in database
GPG key ID: 32B47AE8F388A1FF
3 changed files with 110 additions and 9 deletions

View file

@ -2,26 +2,45 @@ require "./arguments"
require "./method_call"
require "./stub"
require "./stubable"
require "./stubbed_name"
require "./unexpected_message"
require "./value_stub"
module Spectator
# Defines the name of a double or mock.
#
# When present on a stubbed type, this annotation indicates its name in output such as exceptions.
# Must have one argument - the name of the double or mock.
# This can be a symbol, string literal, or type name.
annotation StubbedName; end
# 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`.
# Doubles should be defined with the `#define` macro.
#
# 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.
abstract class Double
include Stubable
Log = Spectator::Log.for(self)
# Defines a test double type.
#
# The *type_name* is the name to give the class.
# Instances of the double can be named by providing a *name*.
# This can be a symbol, string, or even a type.
# See `StubbedName` for details.
#
# After the names, a collection of key-value pairs can be given to quickly define methods.
# Each key is the method name, and the corresponding value is the value returned by the method.
# These methods accept any arguments.
# Additionally, these methods can be overridden later with stubs.
#
# Lastly, a block can be provided to define additional methods and stubs.
# The block is evaluated in the context of the double's class.
#
# ```
# Double.define(SomeDouble, meth1: 42, meth2: "foobar") do
# stub abstract def meth3 : Symbol
# end
# ```
macro define(type_name, name = nil, **value_methods, &block)
{% if name %}@[::Spectator::StubbedName({{name}})]{% end %}
class {{type_name.id}} < {{@type.name}}
@ -34,21 +53,26 @@ module Spectator
end
end
# Creates the double.
#
# An initial set of *stubs* can be provided.
def initialize(@stubs : Array(Stub) = [] of Stub)
end
# Defines a stub to change the behavior of a method in this double.
#
# NOTE: Defining a stub for a method not defined in the double's type has no effect.
protected def _spectator_define_stub(stub : Stub) : Nil
@stubs.unshift(stub)
end
private def _spectator_find_stub(call) : Stub?
private def _spectator_find_stub(call : MethodCall) : Stub?
Log.debug { "Finding stub for #{call}" }
stub = @stubs.find &.===(call)
Log.debug { stub ? "Found stub #{stub} for #{call}" : "Did not find stub for #{call}" }
stub
end
# Utility returning the double's name as a string.
private def _spectator_stubbed_name : String
{% if anno = @type.annotation(StubbedName) %}
"#<Double " + {{(anno[0] || :Anonymous.id).stringify}} + ">"

View file

@ -1,5 +1,49 @@
module Spectator
# Mix-in for mocks and doubles providing method stubs.
#
# Macros in this module can override existing methods.
# Stubbed methods will look for stubs to evaluate in place of their original functionality.
# The primary macro of interest is `#stub`.
# The macros are intended to be called from within the type being stubbed.
#
# Types including this module must define `#_spectator_find_stub` and `#_spectator_stubbed_name`.
# These are internal, reserved method names by Spectator, hence the `_spectator` prefix.
# These methods can't (and shouldn't) be stubbed.
module Stubable
# Attempts to find a stub that satisfies a method call.
#
# Returns a stub that matches the method *call*
# or nil if no stubs satisfy it.
abstract def _spectator_find_stub(call : MethodCall) : Stub?
# Utility returning the mock or double's name as a string.
abstract def _spectator_stubbed_name : String
# Redefines a method to accept stubs.
#
# The *method* should be a `Def`.
# That is, a normal looking method definition should follow the `stub` keyword.
#
# ```
# stub def stubbed_method
# "foobar"
# end
# ```
#
# The method being stubbed must already exist in the type, parent, or included/extend module.
# If it doesn't exist, and a new stubable method is being added, use `#inject_stub` instead.
# The original's method is called if there are no applicable stubs for the invocation.
# The body of the method passed to this macro is ignored.
#
# The method can be abstract.
# If an abstract method is invoked that doesn't have a stub, an `UnexpectedMessage` error is raised.
# The abstract method should have a return type annotation, otherwise the compiled return type will probably end up as a giant union.
#
# ```
# stub abstract def stubbed_method : String
# ```
#
# Stubbed methods will call `#_spectator_find_stub` with the method call information.
private macro stub(method)
{% raise "stub requires a method definition" if !method.is_a?(Def) %}
{% raise "Cannot stub method with reserved keyword as name - #{method.name}" if ::Spectator::DSL::RESERVED_KEYWORDS.includes?(method.name.symbolize) %}
@ -40,6 +84,29 @@ module Spectator
end
end
# Redefines a method to require stubs.
#
# This macro is similar to `#stub` but requires that a stub is defined for the method if it's called.
#
# The *method* should be a `Def`.
# That is, a normal looking method definition should follow the `stub` keyword.
#
# ```
# abstract_stub def stubbed_method
# "foobar"
# end
# ```
#
# The method being stubbed doesn't need to exist yet.
# Its body of the method passed to this macro is ignored.
# The method can be abstract.
# It should have a return type annotation, otherwise the compiled return type will probably end up as a giant union.
#
# ```
# abstract_stub abstract def stubbed_method : String
# ```
#
# Stubbed methods will call `#_spectator_find_stub` with the method call information.
private macro abstract_stub(method)
{% raise "abstract_stub requires a method definition" if !method.is_a?(Def) %}
{% raise "Cannot stub method with reserved keyword as name - #{method.name}" if ::Spectator::DSL::RESERVED_KEYWORDS.includes?(method.name.symbolize) %}
@ -72,6 +139,7 @@ module Spectator
end
end
# Utility for defining a stubbed method and a fallback.
private macro inject_stub(method)
{{method}}
stub {{method}}

View file

@ -0,0 +1,9 @@
module Spectator
# Defines the name of a double or mock.
#
# When present on a stubbed type, this annotation indicates its name in output such as exceptions.
# Must have one argument - the name of the double or mock.
# This can be a symbol, string literal, or type name.
annotation StubbedName
end
end