From cecd2464de18c792b1538b90dca18da259975ac7 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 3 Jul 2022 13:40:29 -0600 Subject: [PATCH] Initial code for StubbedType --- spec/spectator/mocks/double_spec.cr | 29 +++++++++++++ src/spectator/mocks/stubbable.cr | 8 ++++ src/spectator/mocks/stubbed_type.cr | 66 +++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+) create mode 100644 src/spectator/mocks/stubbed_type.cr diff --git a/spec/spectator/mocks/double_spec.cr b/spec/spectator/mocks/double_spec.cr index 008320a..841d8e4 100644 --- a/spec/spectator/mocks/double_spec.cr +++ b/spec/spectator/mocks/double_spec.cr @@ -270,6 +270,35 @@ Spectator.describe Spectator::Double do end end + context "class method stubs" do + Spectator::Double.define(ClassDouble) do + stub def self.foo + :stub + end + + stub def self.bar(arg) + arg + end + + stub def self.baz + yield + end + end + + subject(dbl) { ClassDouble } + let(foo_stub) { Spectator::ValueStub.new(:foo, :override) } + + after_each { dbl._spectator_clear_stubs } + + it "overrides an existing method" do + expect { dbl._spectator_define_stub(foo_stub) }.to change { dbl.foo }.from(:stub).to(:override) + end + + it "doesn't affect other methods" do + expect { dbl._spectator_define_stub(foo_stub) }.to_not change { dbl.bar(42) } + end + end + describe "#_spectator_define_stub" do subject(dbl) { FooBarDouble.new } let(stub3) { Spectator::ValueStub.new(:foo, 3) } diff --git a/src/spectator/mocks/stubbable.cr b/src/spectator/mocks/stubbable.cr index d3e1a1f..2dca87f 100644 --- a/src/spectator/mocks/stubbable.cr +++ b/src/spectator/mocks/stubbable.cr @@ -2,6 +2,7 @@ require "../dsl/reserved" require "./arguments" require "./method_call" require "./stub" +require "./stubbed_type" require "./typed_stub" module Spectator @@ -380,5 +381,12 @@ module Spectator end end end + + # Automatically extend `StubbedType` when a type is made stubbable. + macro included + extend StubbedType + + private class_getter _spectator_stubs : Array(::Spectator::Stub) = [] of ::Spectator::Stub + end end end diff --git a/src/spectator/mocks/stubbed_type.cr b/src/spectator/mocks/stubbed_type.cr new file mode 100644 index 0000000..23a4342 --- /dev/null +++ b/src/spectator/mocks/stubbed_type.cr @@ -0,0 +1,66 @@ +require "./method_call" +require "./stub" + +module Spectator + # Defines stubbing functionality at the type level (classes and structs). + # + # This module is intended to be extended from when a type includes `Stubbable`. + module StubbedType + private abstract def _spectator_stubs : Array(Stub) + + def _spectator_find_stub(call : MethodCall) : Stub? + _spectator_stubs.find &.===(call) + end + + def _spectator_stub_for_method?(method : Symbol) : Bool + _spectator_stubs.any? { |stub| stub.method == method } + end + + def _spectator_define_stub(stub : Stub) : Nil + _spectator_stubs.unshift(stub) + end + + def _spectator_clear_stubs : Nil + _spectator_stubs.clear + end + + def _spectator_record_call(call : MethodCall) : Nil + _spectator_calls << call + end + + def _spectator_calls + [] of MethodCall + end + + def _spectator_stub_fallback(call : MethodCall, &) + Log.trace { "Fallback for #{call} - call original" } + yield + end + + def _spectator_stub_fallback(call : MethodCall, type, &) + _spectator_stub_fallback(call) { yield } + end + + def _spectator_abstract_stub_fallback(call : MethodCall) + Log.info do + break unless _spectator_stub_for_method?(call.method) + + "Stubs are defined for #{call.method.inspect}, but none matched (no argument constraints met)." + end + + raise UnexpectedMessage.new("#{_spectator_stubbed_name} received unexpected message #{call}") + end + + def _spectator_abstract_stub_fallback(call : MethodCall, type) + _spectator_abstract_stub_fallback(call) + end + + def _spectator_stubbed_name : String + {% if anno = @type.annotation(StubbedName) %} + "#" + {% else %} + "#" + {% end %} + end + end +end