Initial implementation of have_received

This commit is contained in:
Michael Miller 2022-07-11 20:25:15 -06:00
parent 4f46c98a86
commit 694e2e6259
No known key found for this signature in database
GPG key ID: 32B47AE8F388A1FF
3 changed files with 159 additions and 0 deletions

View file

@ -0,0 +1,70 @@
require "../../../spec_helper"
Spectator.describe "Stubbable receiver DSL" do
context "with a double" do
double(:dbl, foo: 42)
let(dbl) { double(:dbl) }
it "matches when a message is received" do
dbl.foo
expect(dbl).to have_received(:foo)
end
it "matches when a message isn't received" do
expect(dbl).to_not have_received(:foo)
end
it "matches when a message is received with matching arguments" do
dbl.foo(:bar)
expect(dbl).to have_received(:foo).with(:bar)
end
it "matches when a message without arguments is received" do
dbl.foo
expect(dbl).to_not have_received(:foo).with(:bar)
end
it "matches when a message without arguments isn't received" do
expect(dbl).to_not have_received(:foo).with(:bar)
end
it "matches when a message with arguments isn't received" do
dbl.foo(:bar)
expect(dbl).to_not have_received(:foo).with(:baz)
end
end
context "with a mock" do
abstract class MyClass
abstract def foo(arg) : Int32
end
mock(MyClass, foo: 42)
let(fake) { mock(MyClass) }
it "matches when a message is received" do
fake.foo(:bar)
expect(fake).to have_received(:foo)
end
it "matches when a message isn't received" do
expect(fake).to_not have_received(:foo)
end
it "matches when a message is received with matching arguments" do
fake.foo(:bar)
expect(fake).to have_received(:foo).with(:bar)
end
it "matches when a message without arguments is received" do
expect(fake).to_not have_received(:foo).with(:bar)
end
it "matches when a message with arguments isn't received" do
fake.foo(:bar)
expect(fake).to_not have_received(:foo).with(:baz)
end
end
end

View file

@ -821,6 +821,12 @@ module Spectator::DSL
expect {{block}}.to raise_error({{type}}, {{message}})
end
# Indicates that a mock or double (stubbable type) should receive a message (have a method called).
# The *method* is the name of the method expected to be called.
#
# ```
# expect(dbl).to have_received(:foo)
# ```
macro have_received(method)
%value = ::Spectator::Value.new(({{method.id.symbolize}}), {{method.id.stringify}})
::Spectator::Matchers::ReceiveMatcher.new(%value)

View file

@ -0,0 +1,83 @@
require "../mocks/stub"
require "../mocks/stubbable"
require "../mocks/stubbed_type"
require "./matcher"
module Spectator::Matchers
# Matcher that inspects stubbable objects for method calls.
struct ReceiveMatcher < Matcher
def initialize(@stub : Stub)
end
def initialize(expected : Expression(Symbol))
stub = NullStub.new(expected.value).as(Stub)
initialize(stub)
end
# Returns a new matcher with an argument constraint.
def with(*args, **kwargs) : self
stub = @stub.with(*args, **kwargs)
self.class.new(stub)
end
# Short text about the matcher's purpose.
def description : String
"received #{@stub}"
end
# Actually performs the test against the expression (value or block).
def match(actual : Expression(Stubbable) | Expression(StubbedType)) : MatchData
stubbed = actual.value
if stubbed._spectator_calls.any? { |call| @stub === call }
SuccessfulMatchData.new("#{actual.label} received #{@stub}")
else
FailedMatchData.new("#{actual.label} received #{@stub}", "#{actual.label} did not receive #{@stub}", values(actual).to_a)
end
end
# Actually performs the test against the expression (value or block).
def match(actual : Expression(T)) : MatchData forall T
{% raise "Value being checked with `have_received` must be stubbable (mock or double)." %}
end
# Performs the test against the expression (value or block), but inverted.
def negated_match(actual : Expression(Stubbable) | Expression(StubbedType)) : MatchData
stubbed = actual.value
if stubbed._spectator_calls.any? { |call| @stub === call }
FailedMatchData.new("#{actual.label} did not receive #{@stub}", "#{actual.label} received #{@stub}", negated_values(actual).to_a)
else
SuccessfulMatchData.new("#{actual.label} did not receive #{@stub}")
end
end
# Performs the test against the expression (value or block), but inverted.
def negated_match(actual : Expression(T)) : MatchData forall T
{% raise "Value being checked with `have_received` must be stubbable (mock or double)." %}
end
private def match_data_description(actual : Expression(T)) : String forall T
match_data_description(actual.label)
end
private def match_data_description(actual_label : String | Symbol) : String
"#{actual_label} #{description}"
end
private def match_data_description(actual_label : Nil) : String
description
end
# Additional information about the match failure.
private def values(actual : Expression(T)) forall T
{
expected: @stub.to_s,
actual: actual.value._spectator_calls.inspect,
}
end
# Additional information about the match failure when negated.
private def negated_values(actual : Expression(T)) forall T
values(actual)
end
end
end