mirror of
https://gitea.invidious.io/iv-org/shard-spectator.git
synced 2024-08-15 00:53:35 +00:00
Add lazy double
This commit is contained in:
parent
162ad4df33
commit
bed84b315d
4 changed files with 397 additions and 13 deletions
60
spec/spectator/dsl/mocks/lazy_double_spec.cr
Normal file
60
spec/spectator/dsl/mocks/lazy_double_spec.cr
Normal file
|
@ -0,0 +1,60 @@
|
|||
require "../../../spec_helper"
|
||||
|
||||
Spectator.describe "Lazy double DSL" do
|
||||
context "specifying methods as keyword args" do
|
||||
subject(dbl) { double(:test, foo: "foobar", bar: 42) }
|
||||
|
||||
it "defines a double with methods" do
|
||||
aggregate_failures do
|
||||
expect(dbl.foo).to eq("foobar")
|
||||
expect(dbl.bar).to eq(42)
|
||||
end
|
||||
end
|
||||
|
||||
context "with an unexpected message" do
|
||||
it "raises an error" do
|
||||
expect { dbl.baz }.to raise_error(Spectator::UnexpectedMessage, /baz/)
|
||||
end
|
||||
|
||||
it "reports the double name" do
|
||||
expect { dbl.baz }.to raise_error(Spectator::UnexpectedMessage, /:test/)
|
||||
end
|
||||
|
||||
it "reports the arguments" do
|
||||
expect { dbl.baz(:xyz, 123, a: "XYZ") }.to raise_error(Spectator::UnexpectedMessage, /\(:xyz, 123, a: "XYZ"\)/)
|
||||
end
|
||||
end
|
||||
|
||||
context "blocks" do
|
||||
it "supports blocks" do
|
||||
aggregate_failures do
|
||||
expect(dbl.foo { nil }).to eq("foobar")
|
||||
expect(dbl.bar { nil }).to eq(42)
|
||||
end
|
||||
end
|
||||
|
||||
it "fails on undefined messages" do
|
||||
expect do
|
||||
dbl.baz { nil }
|
||||
end.to raise_error(Spectator::UnexpectedMessage, /baz/)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "double naming" do
|
||||
it "accepts a symbolic double name" do
|
||||
dbl = double(:name)
|
||||
expect { dbl.oops }.to raise_error(Spectator::UnexpectedMessage, /:name/)
|
||||
end
|
||||
|
||||
it "accepts a string double name" do
|
||||
dbl = double("Name")
|
||||
expect { dbl.oops }.to raise_error(Spectator::UnexpectedMessage, /"Name"/)
|
||||
end
|
||||
|
||||
it "accepts no name" do
|
||||
dbl = double
|
||||
expect { dbl.oops }.to raise_error(Spectator::UnexpectedMessage, /anonymous/i)
|
||||
end
|
||||
end
|
||||
end
|
264
spec/spectator/mocks/lazy_double_spec.cr
Normal file
264
spec/spectator/mocks/lazy_double_spec.cr
Normal file
|
@ -0,0 +1,264 @@
|
|||
require "../../spec_helper"
|
||||
|
||||
Spectator.describe Spectator::LazyDouble do
|
||||
context "plain double" do
|
||||
subject(dbl) { Spectator::LazyDouble.new("dbl-name", foo: 42, bar: "baz") }
|
||||
|
||||
it "responds to defined messages" do
|
||||
aggregate_failures do
|
||||
expect(dbl.foo).to eq(42)
|
||||
expect(dbl.bar).to eq("baz")
|
||||
end
|
||||
end
|
||||
|
||||
it "fails on undefined messages" do
|
||||
expect { dbl.baz }.to raise_error(Spectator::UnexpectedMessage, /baz/)
|
||||
end
|
||||
|
||||
it "reports the name in errors" do
|
||||
expect { dbl.baz }.to raise_error(/"dbl-name"/)
|
||||
end
|
||||
|
||||
it "reports arguments" do
|
||||
expect { dbl.baz(123, "qux", field: :value) }.to raise_error(Spectator::UnexpectedMessage, /\(123, "qux", field: :value\)/)
|
||||
end
|
||||
|
||||
context "blocks" do
|
||||
it "supports blocks" do
|
||||
aggregate_failures do
|
||||
expect(dbl.foo { nil }).to eq(42)
|
||||
expect(dbl.bar { nil }).to eq("baz")
|
||||
end
|
||||
end
|
||||
|
||||
it "fails on undefined messages" do
|
||||
expect do
|
||||
dbl.baz { nil }
|
||||
end.to raise_error(Spectator::UnexpectedMessage, /baz/)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "without a double name" do
|
||||
subject(dbl) { Spectator::LazyDouble.new }
|
||||
|
||||
it "reports as anonymous" do
|
||||
expect { dbl.baz }.to raise_error(/anonymous/i)
|
||||
end
|
||||
end
|
||||
|
||||
context "with nillable values" do
|
||||
subject(dbl) { Spectator::LazyDouble.new(foo: nil.as(String?), bar: nil) }
|
||||
|
||||
it "doesn't raise on nil" do
|
||||
aggregate_failures do
|
||||
expect(dbl.foo).to be_nil
|
||||
expect(dbl.bar).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with common object methods" do
|
||||
subject(dbl) do
|
||||
Spectator::LazyDouble.new(nil, [
|
||||
Spectator::ValueStub.new(:"!=", "!="),
|
||||
Spectator::ValueStub.new(:"!~", "!~"),
|
||||
Spectator::ValueStub.new(:"==", "=="),
|
||||
Spectator::ValueStub.new(:"===", "==="),
|
||||
Spectator::ValueStub.new(:"=~", "=~"),
|
||||
Spectator::ValueStub.new(:class, "class"),
|
||||
Spectator::ValueStub.new(:dup, "dup"),
|
||||
Spectator::ValueStub.new(:hash, "hash"),
|
||||
Spectator::ValueStub.new(:"in?", true),
|
||||
Spectator::ValueStub.new(:inspect, "inspect"),
|
||||
Spectator::ValueStub.new(:itself, "itself"),
|
||||
Spectator::ValueStub.new(:"not_nil!", "not_nil!"),
|
||||
Spectator::ValueStub.new(:pretty_inspect, "pretty_inspect"),
|
||||
Spectator::ValueStub.new(:tap, "tap"),
|
||||
Spectator::ValueStub.new(:to_json, "to_json"),
|
||||
Spectator::ValueStub.new(:to_pretty_json, "to_pretty_json"),
|
||||
Spectator::ValueStub.new(:to_s, "to_s"),
|
||||
Spectator::ValueStub.new(:to_yaml, "to_yaml"),
|
||||
Spectator::ValueStub.new(:try, "try"),
|
||||
Spectator::ValueStub.new(:object_id, 42_u64),
|
||||
Spectator::ValueStub.new(:"same?", true),
|
||||
] of Spectator::Stub)
|
||||
end
|
||||
|
||||
it "responds with defined messages" do
|
||||
aggregate_failures do
|
||||
expect(dbl.!=(42)).to eq("!=")
|
||||
expect(dbl.!~(42)).to eq("!~")
|
||||
expect(dbl.==(42)).to eq("==")
|
||||
expect(dbl.===(42)).to eq("===")
|
||||
expect(dbl.=~(42)).to eq("=~")
|
||||
expect(dbl.class).to eq("class")
|
||||
expect(dbl.dup).to eq("dup")
|
||||
expect(dbl.hash(42)).to eq("hash")
|
||||
expect(dbl.hash).to eq("hash")
|
||||
expect(dbl.in?(42)).to eq(true)
|
||||
expect(dbl.in?(1, 2, 3)).to eq(true)
|
||||
expect(dbl.inspect).to eq("inspect")
|
||||
expect(dbl.itself).to eq("itself")
|
||||
expect(dbl.not_nil!).to eq("not_nil!")
|
||||
expect(dbl.pretty_inspect).to eq("pretty_inspect")
|
||||
expect(dbl.tap { nil }).to eq("tap")
|
||||
expect(dbl.to_json).to eq("to_json")
|
||||
expect(dbl.to_pretty_json).to eq("to_pretty_json")
|
||||
expect(dbl.to_s).to eq("to_s")
|
||||
expect(dbl.to_yaml).to eq("to_yaml")
|
||||
expect(dbl.try { nil }).to eq("try")
|
||||
expect(dbl.object_id).to eq(42_u64)
|
||||
expect(dbl.same?(dbl)).to eq(true)
|
||||
expect(dbl.same?(nil)).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
it "has a non-union return type" do
|
||||
expect(dbl.inspect).to compile_as(String)
|
||||
end
|
||||
end
|
||||
|
||||
context "without common object methods" do
|
||||
subject(dbl) { Spectator::LazyDouble.new }
|
||||
|
||||
it "raises with undefined messages" do
|
||||
io = IO::Memory.new
|
||||
pp = PrettyPrint.new(io)
|
||||
aggregate_failures do
|
||||
expect { dbl.!=(42) }.to raise_error(Spectator::UnexpectedMessage)
|
||||
expect { dbl.!~(42) }.to raise_error(Spectator::UnexpectedMessage)
|
||||
expect { dbl.==(42) }.to raise_error(Spectator::UnexpectedMessage)
|
||||
expect { dbl.===(42) }.to raise_error(Spectator::UnexpectedMessage)
|
||||
expect { dbl.=~(42) }.to raise_error(Spectator::UnexpectedMessage)
|
||||
expect { dbl.class }.to raise_error(Spectator::UnexpectedMessage)
|
||||
expect { dbl.dup }.to raise_error(Spectator::UnexpectedMessage)
|
||||
expect { dbl.hash(42) }.to raise_error(Spectator::UnexpectedMessage)
|
||||
expect { dbl.hash }.to raise_error(Spectator::UnexpectedMessage)
|
||||
expect { dbl.in?(42) }.to raise_error(Spectator::UnexpectedMessage)
|
||||
expect { dbl.in?(1, 2, 3) }.to raise_error(Spectator::UnexpectedMessage)
|
||||
expect { dbl.inspect }.to raise_error(Spectator::UnexpectedMessage)
|
||||
expect { dbl.itself }.to raise_error(Spectator::UnexpectedMessage)
|
||||
expect { dbl.not_nil! }.to raise_error(Spectator::UnexpectedMessage)
|
||||
expect { dbl.pretty_inspect }.to raise_error(Spectator::UnexpectedMessage)
|
||||
expect { dbl.pretty_inspect(io) }.to raise_error(Spectator::UnexpectedMessage)
|
||||
expect { dbl.pretty_print(pp) }.to raise_error(Spectator::UnexpectedMessage)
|
||||
expect { dbl.tap { nil } }.to raise_error(Spectator::UnexpectedMessage)
|
||||
expect { dbl.to_json }.to raise_error(Spectator::UnexpectedMessage)
|
||||
expect { dbl.to_json(io) }.to raise_error(Spectator::UnexpectedMessage)
|
||||
expect { dbl.to_pretty_json }.to raise_error(Spectator::UnexpectedMessage)
|
||||
expect { dbl.to_pretty_json(io) }.to raise_error(Spectator::UnexpectedMessage)
|
||||
expect { dbl.to_s }.to raise_error(Spectator::UnexpectedMessage)
|
||||
expect { dbl.to_s(io) }.to raise_error(Spectator::UnexpectedMessage)
|
||||
expect { dbl.to_yaml }.to raise_error(Spectator::UnexpectedMessage)
|
||||
expect { dbl.to_yaml(io) }.to raise_error(Spectator::UnexpectedMessage)
|
||||
expect { dbl.try { nil } }.to raise_error(Spectator::UnexpectedMessage)
|
||||
expect { dbl.object_id }.to raise_error(Spectator::UnexpectedMessage)
|
||||
expect { dbl.same?(dbl) }.to raise_error(Spectator::UnexpectedMessage)
|
||||
expect { dbl.same?(nil) }.to raise_error(Spectator::UnexpectedMessage)
|
||||
end
|
||||
end
|
||||
|
||||
it "reports arguments" do
|
||||
expect { dbl.same?(123) }.to raise_error(Spectator::UnexpectedMessage, /\(123\)/)
|
||||
end
|
||||
end
|
||||
|
||||
context "with arguments constraints" do
|
||||
let(arguments) { Spectator::Arguments.capture(/foo/) }
|
||||
|
||||
context "without common object methods" do
|
||||
let(stub) { Spectator::ValueStub.new(:foo, "bar", arguments).as(Spectator::Stub) }
|
||||
subject(dbl) { Spectator::LazyDouble.new(nil, [stub], foo: "fallback") }
|
||||
|
||||
it "returns the response when constraint satisfied" do
|
||||
expect(dbl.foo("foobar")).to eq("bar")
|
||||
end
|
||||
|
||||
it "returns the fallback value when constraint unsatisfied" do
|
||||
expect { dbl.foo("baz") }.to eq("fallback")
|
||||
end
|
||||
|
||||
it "returns the fallback value when argument count doesn't match" do
|
||||
expect { dbl.foo }.to eq("fallback")
|
||||
end
|
||||
end
|
||||
|
||||
context "with common object methods" do
|
||||
let(stub) { Spectator::ValueStub.new(:"same?", true, arguments).as(Spectator::Stub) }
|
||||
subject(dbl) { Spectator::LazyDouble.new(nil, [stub]) }
|
||||
|
||||
it "returns the response when constraint satisfied" do
|
||||
expect(dbl.same?("foobar")).to eq(true)
|
||||
end
|
||||
|
||||
it "raises an error when constraint unsatisfied" do
|
||||
expect { dbl.same?("baz") }.to raise_error(Spectator::UnexpectedMessage)
|
||||
end
|
||||
|
||||
it "raises an error when argument count doesn't match" do
|
||||
expect { dbl.same? }.to raise_error(Spectator::UnexpectedMessage)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#_spectator_define_stub" do
|
||||
subject(dbl) { Spectator::LazyDouble.new(foo: 42, bar: "baz") }
|
||||
let(stub3) { Spectator::ValueStub.new(:foo, 3) }
|
||||
let(stub5) { Spectator::ValueStub.new(:foo, 5) }
|
||||
let(stub7) { Spectator::ValueStub.new(:foo, 7, Spectator::Arguments.capture(:lucky)) }
|
||||
|
||||
it "overrides an existing method" do
|
||||
expect { dbl._spectator_define_stub(stub3) }.to change { dbl.foo }.from(42).to(3)
|
||||
end
|
||||
|
||||
it "replaces an existing stub" do
|
||||
dbl._spectator_define_stub(stub3)
|
||||
expect { dbl._spectator_define_stub(stub5) }.to change { dbl.foo }.from(3).to(5)
|
||||
end
|
||||
|
||||
it "doesn't affect other methods" do
|
||||
expect { dbl._spectator_define_stub(stub5) }.to_not change { dbl.bar }
|
||||
end
|
||||
|
||||
it "picks the correct stub based on arguments" do
|
||||
dbl._spectator_define_stub(stub5)
|
||||
dbl._spectator_define_stub(stub7)
|
||||
aggregate_failures do
|
||||
expect(dbl.foo).to eq(5)
|
||||
expect(dbl.foo(:lucky)).to eq(7)
|
||||
end
|
||||
end
|
||||
|
||||
it "only uses a stub if an argument constraint is met" do
|
||||
dbl._spectator_define_stub(stub7)
|
||||
aggregate_failures do
|
||||
expect(dbl.foo).to eq(42)
|
||||
expect(dbl.foo(:lucky)).to eq(7)
|
||||
end
|
||||
end
|
||||
|
||||
it "ignores the block argument if not in the constraint" do
|
||||
dbl._spectator_define_stub(stub5)
|
||||
dbl._spectator_define_stub(stub7)
|
||||
aggregate_failures do
|
||||
expect(dbl.foo { nil }).to eq(5)
|
||||
expect(dbl.foo(:lucky) { nil }).to eq(7)
|
||||
end
|
||||
end
|
||||
|
||||
context "with previously undefined methods" do
|
||||
it "can stub methods" do
|
||||
stub = Spectator::ValueStub.new(:baz, :xyz)
|
||||
dbl._spectator_define_stub(stub)
|
||||
expect(dbl.baz).to eq(:xyz)
|
||||
end
|
||||
|
||||
it "uses a stub only if an argument constraint is met" do
|
||||
stub = Spectator::ValueStub.new(:baz, :xyz, Spectator::Arguments.capture(:right))
|
||||
dbl._spectator_define_stub(stub)
|
||||
expect { dbl.baz }.to raise_error(Spectator::UnexpectedMessage, /baz/)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -33,7 +33,7 @@ module Spectator::DSL
|
|||
{% end %}
|
||||
end
|
||||
|
||||
private macro new_double(name)
|
||||
private macro new_double(name = nil, **value_methods)
|
||||
{% # Find tuples with the same name.
|
||||
found_tuples = ::Spectator::DSL::Mocks::DOUBLES.select { |tuple| tuple[0] == name.id.symbolize }
|
||||
|
||||
|
@ -57,25 +57,25 @@ module Spectator::DSL
|
|||
found_tuples = found_tuples.sort_by do |tuple|
|
||||
tuple[1].id.split("::").size
|
||||
end
|
||||
found_tuple = found_tuples.last
|
||||
raise "Undefined double type '#{name}'" unless found_tuple
|
||||
found_tuple = found_tuples.last %}
|
||||
|
||||
# Store the type name used to define the underlying double.
|
||||
double_type_name = found_tuple[2].id %}
|
||||
|
||||
{{double_type_name}}.new
|
||||
{% if found_tuple %}
|
||||
{{found_tuple[2].id}}.new
|
||||
{% else %}
|
||||
::Spectator::LazyDouble.new({{name}}, {{**value_methods}})
|
||||
{% end %}
|
||||
end
|
||||
|
||||
macro double(name, **value_methods, &block)
|
||||
{% if @def %}
|
||||
new_double({{name}}){% if block %} do
|
||||
{{block.body}}
|
||||
end{% end %}
|
||||
{% else %}
|
||||
def_double({{name}}, {{**value_methods}}){% if block %} do
|
||||
{% begin %}
|
||||
{% if @def %}new_double{% else %}def_double{% end %}({{name}}, {{**value_methods}}){% if block %} do
|
||||
{{block.body}}
|
||||
end{% end %}
|
||||
{% end %}
|
||||
end
|
||||
|
||||
macro double(**value_methods)
|
||||
new_double({{**value_methods}})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
60
src/spectator/mocks/lazy_double.cr
Normal file
60
src/spectator/mocks/lazy_double.cr
Normal file
|
@ -0,0 +1,60 @@
|
|||
require "../label"
|
||||
require "./arguments"
|
||||
require "./double"
|
||||
require "./method_call"
|
||||
require "./stub"
|
||||
|
||||
module Spectator
|
||||
# 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`.
|
||||
#
|
||||
# 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.
|
||||
class LazyDouble(Messages) < Double
|
||||
@name : String?
|
||||
|
||||
def initialize(_spectator_double_name = nil, _spectator_double_stubs = [] of Stub, **@messages : **Messages)
|
||||
super(_spectator_double_stubs)
|
||||
@name = _spectator_double_name.try &.inspect
|
||||
end
|
||||
|
||||
# Returns the double's name formatted for user output.
|
||||
private def _spectator_stubbed_name : String
|
||||
"#<LazyDouble #{@name || "Anonymous"}>"
|
||||
end
|
||||
|
||||
# Handles all messages.
|
||||
macro method_missing(call)
|
||||
# Capture information about the call.
|
||||
%args = ::Spectator::Arguments.capture({{call.args.splat(", ")}}{% if call.named_args %}{{*call.named_args}}{% end %})
|
||||
%call = ::Spectator::MethodCall.new({{call.name.symbolize}}, %args)
|
||||
|
||||
# Attempt to find a stub that satisfies the method call and arguments.
|
||||
if %stub = _spectator_find_stub(%call)
|
||||
# Cast the stub or return value to the expected type.
|
||||
# This is necessary to match the expected return type of the original message.
|
||||
\{% if Messages.keys.includes?({{call.name.symbolize}}) %}
|
||||
_spectator_cast_stub_value(%stub, %call, \{{Messages[{{call.name.symbolize}}.id]}}, \{{Messages[{{call.name.symbolize}}.id].resolve >= Nil}})
|
||||
\{% else %}
|
||||
# A method that was not defined during initialization was stubbed.
|
||||
# Return the value of the stub as-is.
|
||||
# Might want to give a warning here, as this may produce a "bloated" union of all known stub types.
|
||||
%stub.value
|
||||
\{% end %}
|
||||
else
|
||||
# A stub wasn't found, invoke the fallback logic.
|
||||
\{% if Messages.keys.includes?({{call.name.symbolize}}.id) %}
|
||||
# Pass along the message type and a block to invoke it.
|
||||
_spectator_stub_fallback(%call, \{{Messages[{{call.name.symbolize}}.id]}}) { @messages[{{call.name.symbolize}}] }
|
||||
\{% else %}
|
||||
# Message received for a methods that isn't stubbed nor defined when initialized.
|
||||
_spectator_abstract_stub_fallback(%call)
|
||||
nil # Necessary for compiler to infer return type as nil. Avoids runtime "can't execute ... `x` has no type errors".
|
||||
\{% end %}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue