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
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 context "with common object methods" do
subject(dbl) do subject(dbl) do
EmptyDouble.new([ EmptyDouble.new([
@ -102,6 +126,7 @@ Spectator.describe Spectator::NullDouble do
io = IO::Memory.new io = IO::Memory.new
pp = PrettyPrint.new(io) pp = PrettyPrint.new(io)
aggregate_failures do 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)) 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.dup)
expect_null_double(dbl, dbl.hash(42)) expect_null_double(dbl, dbl.hash(42))
expect_null_double(dbl, dbl.hash) 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.itself)
expect_null_double(dbl, dbl.not_nil!) 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.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_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 end
end end
@ -135,7 +149,7 @@ Spectator.describe Spectator::NullDouble do
context "without common object methods" do context "without common object methods" do
Spectator::NullDouble.define(TestDouble) do Spectator::NullDouble.define(TestDouble) do
abstract_stub abstract def foo(value) : String abstract_stub abstract def foo(value)
end end
let(stub) { Spectator::ValueStub.new(:foo, "bar", arguments).as(Spectator::Stub) } 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 it "returns self when argument count doesn't match" do
expect_null_double(dbl, dbl.foo) expect_null_double(dbl, dbl.foo)
end end
it "has a union return type with self" do
expect(dbl.foo("foobar")).to compile_as(String | TestDouble)
end
end end
context "with common object methods" do context "with common object methods" do
@ -167,7 +177,7 @@ Spectator.describe Spectator::NullDouble do
subject(dbl) { TestDouble.new([stub]) } subject(dbl) { TestDouble.new([stub]) }
it "returns the response when constraint satisfied" do it "returns the response when constraint satisfied" do
expect(dbl.hash("foobar")).to eq(true) expect(dbl.hash("foobar")).to eq(12345)
end end
it "returns self when constraint unsatisfied" do 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 it "returns self when argument count doesn't match" do
expect_null_double(dbl, dbl.hash) expect_null_double(dbl, dbl.hash)
end end
it "has a union return type with self" do
expect(dbl.hash("foobar")).to compile_as(Int32 | TestDouble)
end
end end
end end

View file

@ -81,6 +81,22 @@ module Spectator
{% end %} {% end %}
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. # "Hide" existing methods and methods from ancestors by overriding them.
macro finished macro finished
stub_all {{@type.name(generic_args: false)}} stub_all {{@type.name(generic_args: false)}}

View file

@ -81,6 +81,30 @@ module Spectator
{% end %} {% end %}
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. # "Hide" existing methods and methods from ancestors by overriding them.
macro finished macro finished
stub_all {{@type.name(generic_args: false)}} 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. # Utility returning the mock or double's name as a string.
abstract def _spectator_stubbed_name : 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. # Redefines a method to accept stubs.
# #
# The *method* should be a `Def`. # The *method* should be a `Def`.
@ -78,11 +114,12 @@ module Spectator
%stub.value %stub.value
{% end %} {% end %}
else else
{% if method.abstract? %} {% if !method.abstract? %}
# Response not configured for this method/message. _spectator_stub_fallback(%call, typeof({{original}})) { {{original}} }
raise ::Spectator::UnexpectedMessage.new("#{_spectator_stubbed_name} received unexpected message :{{method.name}} with #{%args}") {% elsif method.return_type %}
_spectator_abstract_stub_fallback(%call, {{method.return_type}})
{% else %} {% else %}
{{original}} _spectator_abstract_stub_fallback(%call)
{% end %} {% end %}
end end
end end
@ -138,8 +175,11 @@ module Spectator
%stub.value %stub.value
{% end %} {% end %}
else else
# Response not configured for this method/message. {% if method.return_type %}
raise ::Spectator::UnexpectedMessage.new("#{_spectator_stubbed_name} received unexpected message :{{method.name}} with #{%args}") _spectator_abstract_stub_fallback(%call, {{method.return_type}})
{% else %}
_spectator_abstract_stub_fallback(%call)
{% end %}
end end
end end
end end