Smarter handling of stub fallback

This commit is contained in:
Michael Miller 2022-03-15 22:46:43 -06:00
parent 01fc91e854
commit 332ab1cebc
No known key found for this signature in database
GPG key ID: AC78B32D30CE34A2
4 changed files with 113 additions and 27 deletions

View file

@ -34,6 +34,30 @@ Spectator.describe Spectator::NullDouble do
end
end
context "with abstract stubs and return type annotations" do
Spectator::NullDouble.define(TestDouble) do
abstract_stub abstract def foo(value) : String
end
let(arguments) { Spectator::Arguments.capture(/foo/) }
let(stub) { Spectator::ValueStub.new(:foo, "bar", arguments).as(Spectator::Stub) }
subject(dbl) { TestDouble.new([stub]) }
it "enforces the return type" do
expect(dbl.foo("foobar")).to compile_as(String)
end
it "raises on non-matching arguments" do
expect { dbl.foo("bar") }.to raise_error(TypeCastError, /String/)
end
it "raises on non-matching stub" do
stub = Spectator::ValueStub.new(:foo, 42, arguments).as(Spectator::Stub)
dbl._spectator_define_stub(stub)
expect { dbl.foo("foobar") }.to raise_error(TypeCastError, /String/)
end
end
context "with common object methods" do
subject(dbl) do
EmptyDouble.new([
@ -102,6 +126,7 @@ Spectator.describe Spectator::NullDouble do
io = IO::Memory.new
pp = PrettyPrint.new(io)
aggregate_failures do
# Methods that would cause type cast errors are omitted from this list.
expect_null_double(dbl, dbl.!=(42))
expect_null_double(dbl, dbl.!~(42))
expect_null_double(dbl, dbl.==(42))
@ -111,21 +136,10 @@ Spectator.describe Spectator::NullDouble do
expect_null_double(dbl, dbl.dup)
expect_null_double(dbl, dbl.hash(42))
expect_null_double(dbl, dbl.hash)
# expect(dbl.in?(42)).to be(dbl)
# expect(dbl.in?(1, 2, 3)).to be(dbl)
expect_null_double(dbl, dbl.inspect)
expect_null_double(dbl, dbl.itself)
expect_null_double(dbl, dbl.not_nil!)
expect_null_double(dbl, dbl.pretty_inspect)
expect_null_double(dbl, dbl.tap { nil })
expect_null_double(dbl, dbl.to_json)
expect_null_double(dbl, dbl.to_pretty_json)
expect_null_double(dbl, dbl.to_s)
expect_null_double(dbl, dbl.to_yaml)
expect_null_double(dbl, dbl.try { nil })
# expect(dbl.object_id).to be(dbl)
# expect(dbl.same?(dbl)).to be(dbl)
# expect(dbl.same?(nil)).to be(dbl)
end
end
end
@ -135,7 +149,7 @@ Spectator.describe Spectator::NullDouble do
context "without common object methods" do
Spectator::NullDouble.define(TestDouble) do
abstract_stub abstract def foo(value) : String
abstract_stub abstract def foo(value)
end
let(stub) { Spectator::ValueStub.new(:foo, "bar", arguments).as(Spectator::Stub) }
@ -152,10 +166,6 @@ Spectator.describe Spectator::NullDouble do
it "returns self when argument count doesn't match" do
expect_null_double(dbl, dbl.foo)
end
it "has a union return type with self" do
expect(dbl.foo("foobar")).to compile_as(String | TestDouble)
end
end
context "with common object methods" do
@ -167,7 +177,7 @@ Spectator.describe Spectator::NullDouble do
subject(dbl) { TestDouble.new([stub]) }
it "returns the response when constraint satisfied" do
expect(dbl.hash("foobar")).to eq(true)
expect(dbl.hash("foobar")).to eq(12345)
end
it "returns self when constraint unsatisfied" do
@ -177,10 +187,6 @@ Spectator.describe Spectator::NullDouble do
it "returns self when argument count doesn't match" do
expect_null_double(dbl, dbl.hash)
end
it "has a union return type with self" do
expect(dbl.hash("foobar")).to compile_as(Int32 | TestDouble)
end
end
end

View file

@ -81,6 +81,22 @@ module Spectator
{% end %}
end
private def _spectator_stub_fallback(call : MethodCall, &)
yield
end
private def _spectator_stub_fallback(call : MethodCall, type, &)
yield
end
private def _spectator_abstract_stub_fallback(call : MethodCall)
raise UnexpectedMessage.new("#{_spectator_stubbed_name} received unexpected message #{call}")
end
private def _spectator_abstract_stub_fallback(call : MethodCall, type)
raise UnexpectedMessage.new("#{_spectator_stubbed_name} received unexpected message #{call}")
end
# "Hide" existing methods and methods from ancestors by overriding them.
macro finished
stub_all {{@type.name(generic_args: false)}}

View file

@ -81,6 +81,30 @@ module Spectator
{% end %}
end
private def _spectator_stub_fallback(call : MethodCall, &)
self
end
private def _spectator_stub_fallback(call : MethodCall, type : self, &)
self
end
private def _spectator_stub_fallback(call : MethodCall, type, &)
yield
end
private def _spectator_abstract_stub_fallback(call : MethodCall)
self
end
private def _spectator_abstract_stub_fallback(call : MethodCall, type : self)
self
end
private def _spectator_abstract_stub_fallback(call : MethodCall, type)
raise TypeCastError.new("#{_spectator_stubbed_name} received message #{call} and is attempting to return `self`, but returned type must be `#{type}`.")
end
# "Hide" existing methods and methods from ancestors by overriding them.
macro finished
stub_all {{@type.name(generic_args: false)}}

View file

@ -19,6 +19,42 @@ module Spectator
# Utility returning the mock or double's name as a string.
abstract def _spectator_stubbed_name : String
# Method called when a stub isn't found.
#
# The received message is captured in *call*.
# Yield to call the original method's implementation.
# The stubbed method returns the value returned by this method.
# This method can also raise an error if it's impossible to return something.
abstract def _spectator_stub_fallback(call : MethodCall, &)
# Method called when a stub isn't found.
#
# The received message is captured in *call*.
# The expected return type is provided by *type*.
# Yield to call the original method's implementation.
# The stubbed method returns the value returned by this method.
# This method can also raise an error if it's impossible to return something.
abstract def _spectator_stub_fallback(call : MethodCall, type, &)
# Method called when a stub isn't found.
#
# This is similar to `#_spectator_stub_fallback`,
# but called when the original (un-stubbed) method isn't available.
# The received message is captured in *call*.
# The stubbed method returns the value returned by this method.
# This method can also raise an error if it's impossible to return something.
abstract def _spectator_abstract_stub_fallback(call : MethodCall)
# Method called when a stub isn't found.
#
# This is similar to `#_spectator_stub_fallback`,
# but called when the original (un-stubbed) method isn't available.
# The received message is captured in *call*.
# The expected return type is provided by *type*.
# The stubbed method returns the value returned by this method.
# This method can also raise an error if it's impossible to return something.
abstract def _spectator_abstract_stub_fallback(call : MethodCall, type)
# Redefines a method to accept stubs.
#
# The *method* should be a `Def`.
@ -78,11 +114,12 @@ module Spectator
%stub.value
{% end %}
else
{% if method.abstract? %}
# Response not configured for this method/message.
raise ::Spectator::UnexpectedMessage.new("#{_spectator_stubbed_name} received unexpected message :{{method.name}} with #{%args}")
{% if !method.abstract? %}
_spectator_stub_fallback(%call, typeof({{original}})) { {{original}} }
{% elsif method.return_type %}
_spectator_abstract_stub_fallback(%call, {{method.return_type}})
{% else %}
{{original}}
_spectator_abstract_stub_fallback(%call)
{% end %}
end
end
@ -138,8 +175,11 @@ module Spectator
%stub.value
{% end %}
else
# Response not configured for this method/message.
raise ::Spectator::UnexpectedMessage.new("#{_spectator_stubbed_name} received unexpected message :{{method.name}} with #{%args}")
{% if method.return_type %}
_spectator_abstract_stub_fallback(%call, {{method.return_type}})
{% else %}
_spectator_abstract_stub_fallback(%call)
{% end %}
end
end
end