Add docs and some more type annotations

This commit is contained in:
Michael Miller 2019-08-09 17:26:53 -06:00
parent db4eaca291
commit 39f253952d
4 changed files with 134 additions and 9 deletions

View file

@ -12,26 +12,34 @@ module Spectator::Expectations
def initialize(@match_data : Matchers::MatchData, @source : Source)
end
# Indicates whether the matcher was satisified.
def satisfied?
@match_data.matched?
end
# Indicates that the expectation was not satisified.
def failure?
!satisfied?
end
# Description of why the match failed.
# If nil, then the match was successful.
def failure_message?
@match_data.as?(Matchers::FailedMatchData).try(&.failure_message)
end
# Description of why the match failed.
def failure_message
failure_message?.not_nil!
end
# Additional information about the match, useful for debug.
# If nil, then the match was successful.
def values?
@match_data.as?(Matchers::FailedMatchData).try(&.values)
end
# Additional information about the match, useful for debug.
def values
values?.not_nil!
end
@ -47,6 +55,7 @@ module Spectator::Expectations
end
end
# Adds failure information to a JSON structure.
private def failed_to_json(failed : Matchers::FailedMatchData, json : ::JSON::Builder)
json.field("failure", failed.failure_message)
json.field("values") do

View file

@ -12,10 +12,14 @@ module Spectator::Matchers
# it { is_expected.to do_something }
# ```
# The phrasing should be such that it reads "it ___."
# where the blank is what is returned by this method.
abstract def description : String
# Actually performs the test against the expression (value or block).
abstract def match(actual : TestExpression(T)) : MatchData forall T
# Performs the test against the expression (value or block), but inverted.
# A successful match with `#match` should normally fail for this method, and vice-versa.
abstract def negated_match(actual : TestExpression(T)) : MatchData forall T
end
end

View file

@ -5,41 +5,103 @@ require "./successful_match_data"
module Spectator::Matchers
# Provides common methods for matchers.
#
# The `#match` and `#negated_match` methods have an implementation
# that is suitable for most matchers.
# Matchers based on this class need to define `#match?` and `#failure_message`.
# If the matcher can be negated,
# the `#failure_message_when_negated` method needs to be overriden.
# Additionally, the `#does_not_match?` method can be specified
# if there's custom behavior for negated matches.
# If the matcher operates on or has extra data that is useful for debug,
# then the `#values` and `#negated_values` methods can be overriden.
# Finally, define a `#description` message that can be used for the one-liner "it" syntax.
abstract struct StandardMatcher < Matcher
# Message displayed when the matcher isn't satisifed.
# This is only called when `#matches?` returns false.
private abstract def failure_message(actual) : String
#
# This is only called when `#match?` returns false.
#
# The message should typically only contain the test expression labels.
# Actual values should be returned by `#values`.
private abstract def failure_message(actual : TestExpression(T)) : String forall T
# Message displayed when the matcher isn't satisifed and is negated.
#
# This is only called when `#does_not_match?` returns false.
#
# A default implementation of this method is provided,
# which causes compilation to fail.
# If the matcher supports negation, it must override this method.
private def failure_message_when_negated(actual) : String
#
# The message should typically only contain the test expression labels.
# Actual values should be returned by `#values`.
private def failure_message_when_negated(actual : TestExpression(T)) : String forall T
{% raise "Negation with #{@type.name} is not supported." %}
end
# Checks whether the matcher is satisifed.
private abstract def match?(actual) : Bool
private abstract def match?(actual : TestExpression(T)) : Bool forall T
# If the expectation is negated, then this method is called instead of `#match?`.
#
# The default implementation of this method is to invert the result of `#match?`.
# If the matcher requires custom handling of negated matches,
# then this method should be overriden.
# Remember to override `#failure_message_when_negated` as well.
private def does_not_match?(actual) : Bool
private def does_not_match?(actual : TestExpression(T)) : Bool forall T
!match?(actual)
end
private def values(actual)
# Additional information about the match failure.
#
# By default, just the actual value is produced.
# The return value must be a NamedTuple with Strings for each value.
# The tuple can be of any size,
# but the keys must be known at compile-time (as Symbols),
# and the values must be strings.
# Generally, the string values are produced by calling `#inspect` on the relevant object.
# It should look like this:
# ```
# {
# expected: "foo",
# actual: "bar",
# }
# ```
#
# The values should typically only contain the test expression values, not the labels.
# Labeled should be returned by `#failure_message`.
private def values(actual : TestExpression(T)) forall T
{actual: actual.value.inspect}
end
private def negated_values(actual)
# Additional information about the match failure when negated.
#
# By default, just the actual value is produced (same as `#values`).
# The return value must be a NamedTuple with Strings for each value.
# The tuple can be of any size,
# but the keys must be known at compile-time (as Symbols),
# and the values must be strings.
# Generally, the string values are produced by calling `#inspect` on the relevant object.
# It should look like this:
# ```
# {
# expected: "Not foo",
# actual: "bar",
# }
# ```
#
# The values should typically only contain the test expression values, not the labels.
# Labeled should be returned by `#failure_message_when_negated`.
private def negated_values(actual : TestExpression(T)) forall T
values(actual)
end
# Actually performs the test against the expression (value or block).
#
# This method calls the abstract `#match?` method.
# If it returns true, then a `SuccessfulMatchData` instance is returned.
# Otherwise, a `FailedMatchData` instance is returned.
# Additionally, `#failure_message` and `#values` are called for a failed match.
def match(actual : TestExpression(T)) : MatchData forall T
if match?(actual)
SuccessfulMatchData.new
@ -48,6 +110,13 @@ module Spectator::Matchers
end
end
# Performs the test against the expression (value or block), but inverted.
# A successful match with `#match` should normally fail for this method, and vice-versa.
#
# This method calls the abstract `#does_not_match?` method.
# If it returns true, then a `SuccessfulMatchData` instance is returned.
# Otherwise, a `FailedMatchData` instance is returned.
# Additionally, `#failure_message_when_negated` and `#negated_values` are called for a failed match.
def negated_match(actual : TestExpression(T)) : MatchData forall T
if does_not_match?(actual)
SuccessfulMatchData.new

View file

@ -3,6 +3,18 @@ require "./standard_matcher"
module Spectator::Matchers
# Category of matcher that uses a value.
# Matchers of this type expect that a SUT applies to the value in some way.
#
# Matchers based on this class need to define `#match?` and `#failure_message`.
# If the matcher can be negated,
# the `#failure_message_when_negated` method needs to be overriden.
# Additionally, the `#does_not_match?` method can be specified
# if there's custom behavior for negated matches.
# If the matcher operates on or has extra data that is useful for debug,
# then the `#values` and `#negated_values` methods can be overriden.
# Finally, define a `#description` message that can be used for the one-liner "it" syntax.
#
# The failure messages should typically only contain the test expression labels.
# Actual values should be returned by `#values` and `#negated_values`.
abstract struct ValueMatcher(ExpectedType) < StandardMatcher
# Expected value.
# Sub-types may use this value to test the expectation and generate message strings.
@ -13,11 +25,42 @@ module Spectator::Matchers
def initialize(@expected : TestValue(ExpectedType))
end
private def values(actual)
# Additional information about the match failure.
#
# By default, just the actual and expected values are produced.
# The return value must be a NamedTuple with Strings for each value.
# The tuple can be of any size,
# but the keys must be known at compile-time (as Symbols),
# and the values must be strings.
# Generally, the string values are produced by calling `#inspect` on the relevant object.
# It should look like this:
# ```
# {
# expected: "foo",
# actual: "bar",
# }
# ```
private def values(actual : TestExpression(T)) forall T
super.merge(expected: expected.value.inspect)
end
private def negated_values(actual)
# Additional information about the match failure when negated.
#
# By default, just the actual and expected values are produced (same as `#values`).
# However, the expected value is prefixed with the word "Not".
# The return value must be a NamedTuple with Strings for each value.
# The tuple can be of any size,
# but the keys must be known at compile-time (as Symbols),
# and the values must be strings.
# Generally, the string values are produced by calling `#inspect` on the relevant object.
# It should look like this:
# ```
# {
# expected: "Not foo",
# actual: "bar",
# }
# ```
private def negated_values(actual : TestExpression(T)) forall T
super.merge(expected: "Not #{expected.value.inspect}")
end
end