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

@ -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