diff --git a/src/spectator/double.cr b/src/spectator/double.cr index 901683b..7423d0a 100644 --- a/src/spectator/double.cr +++ b/src/spectator/double.cr @@ -1,26 +1,50 @@ +require "./method_stub" + module Spectator abstract class Double + @stubs = Deque(MethodStub).new + + private macro delegate_internal(method, *args) + # Modified version of Object#delegate + {% if method.id.ends_with?('=') && method.id != "[]=" %} + @internal.{{method.id}} {{args.splat}} + {% else %} + @internal.{{method.id}}({{args.splat}}) + {% end %} + end + macro stub(definition, &block) {% name = nil + params = nil args = nil body = nil if definition.is_a?(Call) # stub foo { :bar } name = definition.name.id - args = definition.args + params = definition.args + args = params.map { |p| p.is_a?(TypeDeclaration) ? p.var : p.id } body = definition.block.is_a?(Nop) ? block : definition.block elsif definition.is_a?(TypeDeclaration) # stub foo : Symbol name = definition.var + params = [] of MacroId args = [] of MacroId body = block else raise "Unrecognized stub format" end %} - delegate {{name}}, to: @internal + + def {{name}}({{params.splat}}) + %stub = @stubs.find(&.callable?({{name.symbolize}}{% unless args.empty? %}, {{args.splat}}{% end %})) + if %stub + %stub.call({{args.splat}}) + else + delegate_internal({{name}}{% unless args.empty? %}, {{args.splat}}{% end %}) + end + end private class Internal - def {{name}}({{args.splat}}) + def {{name}}({{params.splat}}) {% if body && !body.is_a?(Nop) %} {{body.body}} {% else %} @@ -29,5 +53,9 @@ module Spectator end end end + + protected def spectator_define_stub(stub : MethodStub) : Nil + @stubs << stub + end end end diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index 0e9ea36..f9088e6 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -1,4 +1,6 @@ require "../double" +require "../generic_method_stub" +require "../open_mock" module Spectator::DSL macro double(name, &block) @@ -21,4 +23,13 @@ module Spectator::DSL end {% end %} end + + def allow(double : ::Spectator::Double) + OpenMock.new(double) + end + + macro receive(method_name, _source_file = __FILE__, _source_line = __LINE__) + %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) + ::Spectator::GenericMethodStub(Nil).new({{method_name.symbolize}}, %source) + end end diff --git a/src/spectator/generic_method_stub.cr b/src/spectator/generic_method_stub.cr new file mode 100644 index 0000000..e9682fd --- /dev/null +++ b/src/spectator/generic_method_stub.cr @@ -0,0 +1,13 @@ +require "./method_stub" +require "./source" + +module Spectator + class GenericMethodStub(ReturnType, *ArgumentTypes) < MethodStub + def callable?(name : Symbol, *args) : Bool + super + end + + def call(*args) + end + end +end diff --git a/src/spectator/method_stub.cr b/src/spectator/method_stub.cr new file mode 100644 index 0000000..7a710a0 --- /dev/null +++ b/src/spectator/method_stub.cr @@ -0,0 +1,15 @@ +require "./source" + +module Spectator + abstract class MethodStub + def initialize(@name : Symbol, @source : Source) + end + + def callable?(name : Symbol, *args) : Bool + name == @name + end + + def call(*args) + end + end +end diff --git a/src/spectator/open_mock.cr b/src/spectator/open_mock.cr new file mode 100644 index 0000000..64068ce --- /dev/null +++ b/src/spectator/open_mock.cr @@ -0,0 +1,10 @@ +module Spectator + struct OpenMock + def initialize(@mock : Double) + end + + def to(stub : MethodStub) : Nil + @mock.spectator_define_stub(stub) + end + end +end