From 93c442d1e235235ff5b03a5baf12972044beb466 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 4 Jan 2020 12:46:08 -0700 Subject: [PATCH] Add instance_of matcher to check exact type --- src/spectator/dsl/matchers.cr | 26 ++++++++++ src/spectator/matchers/instance_matcher.cr | 57 ++++++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 src/spectator/matchers/instance_matcher.cr diff --git a/src/spectator/dsl/matchers.cr b/src/spectator/dsl/matchers.cr index f37ada2..4eb83e8 100644 --- a/src/spectator/dsl/matchers.cr +++ b/src/spectator/dsl/matchers.cr @@ -122,6 +122,32 @@ module Spectator be_a({{expected}}) end + # Indicates that some value should be of a specified type. + # The value's runtime class is checked. + # A type name or type union should be used for *expected*. + # + # Examples: + # ``` + # expect(123).to be_instance_of(Int32) + # ``` + macro be_instance_of(expected) + ::Spectator::Matchers::InstanceMatcher({{expected}}).new + end + + # Indicates that some value should be of a specified type. + # The value's runtime class is checked. + # A type name or type union should be used for *expected*. + # This method is identical to `#be_an_instance_of`, + # and exists just to improve grammar. + # + # Examples: + # ``` + # expect(123).to be_an_instance_of(Int32) + # ``` + macro be_an_instance_of(expected) + be_instance_of({{expected}}) + end + # Indicates that some value should respond to a method call. # One or more method names can be provided. # diff --git a/src/spectator/matchers/instance_matcher.cr b/src/spectator/matchers/instance_matcher.cr new file mode 100644 index 0000000..c77531e --- /dev/null +++ b/src/spectator/matchers/instance_matcher.cr @@ -0,0 +1,57 @@ +require "./matcher" + +module Spectator::Matchers + # Matcher that tests a value is of a specified type. + struct InstanceMatcher(Expected) < StandardMatcher + # Short text about the matcher's purpose. + # This explains what condition satisfies the matcher. + # The description is used when the one-liner syntax is used. + def description : String + "is an instance of #{Expected}" + end + + # Checks whether the matcher is satisifed with the expression given to it. + private def match?(actual : TestExpression(T)) : Bool forall T + actual.value.class == Expected + end + + # Message displayed when the matcher isn't satisifed. + # + # 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 def failure_message(actual) : String + "#{actual.label} is not an instance of #{Expected}" + end + + # Message displayed when the matcher isn't satisifed and is negated. + # This is essentially what would satisfy the matcher if it wasn't negated. + # + # This is only called when `#does_not_match?` returns false. + # + # The message should typically only contain the test expression labels. + # Actual values should be returned by `#values`. + private def failure_message_when_negated(actual) : String + "#{actual.label} is an instance of #{Expected}" + end + + # Additional information about the match failure. + # The return value is a NamedTuple with Strings for each value. + private def values(actual) + { + expected: Expected.to_s, + actual: actual.value.class.inspect, + } + end + + # Additional information about the match failure when negated. + # The return value is a NamedTuple with Strings for each value. + private def negated_values(actual) + { + expected: "Not #{Expected}", + actual: actual.value.class.inspect, + } + end + end +end