diff --git a/spec/matchers/attributes_matcher_spec.cr b/spec/matchers/attributes_matcher_spec.cr index 473eb7c..d3c9afc 100644 --- a/spec/matchers/attributes_matcher_spec.cr +++ b/spec/matchers/attributes_matcher_spec.cr @@ -1,282 +1,362 @@ require "../spec_helper" describe Spectator::Matchers::AttributesMatcher do - describe "#match?" do + describe "#match" do it "uses ===" do array = %i[a b c] spy = SpySUT.new partial = new_partial(array) - matcher = Spectator::Matchers::HaveMatcher.new({spy}) - matcher.match?(partial).should be_true + matcher = Spectator::Matchers::AttributesMatcher.new({first: spy}) + matcher.match(partial) spy.case_eq_call_count.should be > 0 end - context "one argument" do - context "against an equal value" do - it "is true" do - array = %i[a b c] - attributes = {first: :a} - partial = new_partial(array) - matcher = Spectator::Matchers::AttributesMatcher.new(attributes) - matcher.match?(partial).should be_true - end - end + context "returned MatchData" do + describe "#matched?" do + context "one argument" do + context "against an equal value" do + it "is true" do + array = %i[a b c] + attributes = {first: :a} + partial = new_partial(array) + matcher = Spectator::Matchers::AttributesMatcher.new(attributes) + match_data = matcher.match(partial) + match_data.matched?.should be_true + end + end - context "against a different value" do - it "is false" do - array = %i[a b c] - attributes = {first: :z} - partial = new_partial(array) - matcher = Spectator::Matchers::AttributesMatcher.new(attributes) - matcher.match?(partial).should be_false - end - end + context "against a different value" do + it "is false" do + array = %i[a b c] + attributes = {first: :z} + partial = new_partial(array) + matcher = Spectator::Matchers::AttributesMatcher.new(attributes) + match_data = matcher.match(partial) + match_data.matched?.should be_false + end + end - context "against a matching type" do - it "is true" do - array = %i[a b c] - attributes = {first: Symbol} - partial = new_partial(array) - matcher = Spectator::Matchers::AttributesMatcher.new(attributes) - matcher.match?(partial).should be_true - end - end + context "against a matching type" do + it "is true" do + array = %i[a b c] + attributes = {first: Symbol} + partial = new_partial(array) + matcher = Spectator::Matchers::AttributesMatcher.new(attributes) + match_data = matcher.match(partial) + match_data.matched?.should be_true + end + end - context "against a non-matching type" do - it "is false" do - array = %i[a b c] - attributes = {first: Int32} - partial = new_partial(array) - matcher = Spectator::Matchers::AttributesMatcher.new(attributes) - matcher.match?(partial).should be_false - end - end + context "against a non-matching type" do + it "is false" do + array = %i[a b c] + attributes = {first: Int32} + partial = new_partial(array) + matcher = Spectator::Matchers::AttributesMatcher.new(attributes) + match_data = matcher.match(partial) + match_data.matched?.should be_false + end + end - context "against a matching regex" do - it "is true" do - array = %w[FOO BAR BAZ] - attributes = {first: /foo/i} - partial = new_partial(array) - matcher = Spectator::Matchers::AttributesMatcher.new(attributes) - matcher.match?(partial).should be_true - end - end + context "against a matching regex" do + it "is true" do + array = %w[FOO BAR BAZ] + attributes = {first: /foo/i} + partial = new_partial(array) + matcher = Spectator::Matchers::AttributesMatcher.new(attributes) + match_data = matcher.match(partial) + match_data.matched?.should be_true + end + end - context "against a non-matching regex" do - it "is false" do - array = %w[FOO BAR BAZ] - attributes = {first: /qux/i} - partial = new_partial(array) - matcher = Spectator::Matchers::AttributesMatcher.new(attributes) - matcher.match?(partial).should be_false - end - end - end - - context "multiple attributes" do - context "against equal values" do - it "is true" do - array = %i[a b c] - attributes = {first: :a, last: :c} - partial = new_partial(array) - matcher = Spectator::Matchers::AttributesMatcher.new(attributes) - matcher.match?(partial).should be_true + context "against a non-matching regex" do + it "is false" do + array = %w[FOO BAR BAZ] + attributes = {first: /qux/i} + partial = new_partial(array) + matcher = Spectator::Matchers::AttributesMatcher.new(attributes) + match_data = matcher.match(partial) + match_data.matched?.should be_false + end + end end - context "matching type" do - context "matching regex" do + context "multiple attributes" do + context "against equal values" do + it "is true" do + array = %i[a b c] + attributes = {first: :a, last: :c} + partial = new_partial(array) + matcher = Spectator::Matchers::AttributesMatcher.new(attributes) + match_data = matcher.match(partial) + match_data.matched?.should be_true + end + + context "matching type" do + context "matching regex" do + it "is true" do + array = [:a, 42, "FOO"] + attributes = {first: Symbol, last: /foo/i} + partial = new_partial(array) + matcher = Spectator::Matchers::AttributesMatcher.new(attributes) + match_data = matcher.match(partial) + match_data.matched?.should be_true + end + end + + context "non-matching regex" do + it "is false" do + array = [:a, 42, "FOO"] + attributes = {first: Symbol, last: /bar/i} + partial = new_partial(array) + matcher = Spectator::Matchers::AttributesMatcher.new(attributes) + match_data = matcher.match(partial) + match_data.matched?.should be_false + end + end + end + + context "non-matching type" do + context "matching regex" do + it "is false" do + array = [:a, 42, "FOO"] + attributes = {first: Float32, last: /foo/i} + partial = new_partial(array) + matcher = Spectator::Matchers::AttributesMatcher.new(attributes) + match_data = matcher.match(partial) + match_data.matched?.should be_false + end + end + + context "non-matching regex" do + it "is false" do + array = [:a, 42, "FOO"] + attributes = {first: Float32, last: /bar/i} + partial = new_partial(array) + matcher = Spectator::Matchers::AttributesMatcher.new(attributes) + match_data = matcher.match(partial) + match_data.matched?.should be_false + end + end + end + end + + context "against one equal value" do + it "is false" do + array = %i[a b c] + attributes = {first: :a, last: :d} + partial = new_partial(array) + matcher = Spectator::Matchers::AttributesMatcher.new(attributes) + match_data = matcher.match(partial) + match_data.matched?.should be_false + end + end + + context "against no equal values" do + it "is false" do + array = %i[a b c] + attributes = {first: :d, last: :e} + partial = new_partial(array) + matcher = Spectator::Matchers::AttributesMatcher.new(attributes) + match_data = matcher.match(partial) + match_data.matched?.should be_false + end + end + + context "against matching types" do it "is true" do array = [:a, 42, "FOO"] - attributes = {first: Symbol, last: /foo/i} + attributes = {first: Symbol, last: String} partial = new_partial(array) matcher = Spectator::Matchers::AttributesMatcher.new(attributes) - matcher.match?(partial).should be_true + match_data = matcher.match(partial) + match_data.matched?.should be_true end end - context "non-matching regex" do + context "against one matching type" do it "is false" do array = [:a, 42, "FOO"] - attributes = {first: Symbol, last: /bar/i} + attributes = {first: Symbol, last: Float32} partial = new_partial(array) matcher = Spectator::Matchers::AttributesMatcher.new(attributes) - matcher.match?(partial).should be_false + match_data = matcher.match(partial) + match_data.matched?.should be_false end end - end - context "non-matching type" do - context "matching regex" do + context "against no matching types" do it "is false" do array = [:a, 42, "FOO"] - attributes = {first: Float32, last: /foo/i} + attributes = {first: Float32, last: Bytes} partial = new_partial(array) matcher = Spectator::Matchers::AttributesMatcher.new(attributes) - matcher.match?(partial).should be_false + match_data = matcher.match(partial) + match_data.matched?.should be_false end end - context "non-matching regex" do + context "against matching regexes" do + it "is true" do + array = %w[FOO BAR BAZ] + attributes = {first: /foo/i, last: /baz/i} + partial = new_partial(array) + matcher = Spectator::Matchers::AttributesMatcher.new(attributes) + match_data = matcher.match(partial) + match_data.matched?.should be_true + end + end + + context "against one matching regex" do it "is false" do - array = [:a, 42, "FOO"] - attributes = {first: Float32, last: /bar/i} + array = %w[FOO BAR BAZ] + attributes = {first: /foo/i, last: /qux/i} partial = new_partial(array) matcher = Spectator::Matchers::AttributesMatcher.new(attributes) - matcher.match?(partial).should be_false + match_data = matcher.match(partial) + match_data.matched?.should be_false + end + end + + context "against no matching regexes" do + it "is false" do + array = %w[FOO BAR] + attributes = {first: /baz/i, last: /qux/i} + partial = new_partial(array) + matcher = Spectator::Matchers::AttributesMatcher.new(attributes) + match_data = matcher.match(partial) + match_data.matched?.should be_false + end + end + + context "against equal and matching type and regex" do + it "is true" do + array = [:a, 42, "FOO"] + attributes = {first: Symbol, last: /foo/i, size: 3} + partial = new_partial(array) + matcher = Spectator::Matchers::AttributesMatcher.new(attributes) + match_data = matcher.match(partial) + match_data.matched?.should be_true end end end end - context "against one equal value" do - it "is false" do - array = %i[a b c] - attributes = {first: :a, last: :d} - partial = new_partial(array) - matcher = Spectator::Matchers::AttributesMatcher.new(attributes) - matcher.match?(partial).should be_false - end - end - - context "against no equal values" do - it "is false" do - array = %i[a b c] - attributes = {first: :d, last: :e} - partial = new_partial(array) - matcher = Spectator::Matchers::AttributesMatcher.new(attributes) - matcher.match?(partial).should be_false - end - end - - context "against matching types" do - it "is true" do - array = [:a, 42, "FOO"] - attributes = {first: Symbol, last: String} - partial = new_partial(array) - matcher = Spectator::Matchers::AttributesMatcher.new(attributes) - matcher.match?(partial).should be_true - end - end - - context "against one matching type" do - it "is false" do - array = [:a, 42, "FOO"] - attributes = {first: Symbol, last: Float32} - partial = new_partial(array) - matcher = Spectator::Matchers::AttributesMatcher.new(attributes) - matcher.match?(partial).should be_false - end - end - - context "against no matching types" do - it "is false" do - array = [:a, 42, "FOO"] - attributes = {first: Float32, last: Bytes} - partial = new_partial(array) - matcher = Spectator::Matchers::AttributesMatcher.new(attributes) - matcher.match?(partial).should be_false - end - end - - context "against matching regexes" do - it "is true" do - array = %w[FOO BAR BAZ] - attributes = {first: /foo/i, last: /baz/i} - partial = new_partial(array) - matcher = Spectator::Matchers::AttributesMatcher.new(attributes) - matcher.match?(partial).should be_true - end - end - - context "against one matching regex" do - it "is false" do - array = %w[FOO BAR BAZ] - attributes = {first: /foo/i, last: /qux/i} - partial = new_partial(array) - matcher = Spectator::Matchers::AttributesMatcher.new(attributes) - matcher.match?(partial).should be_false - end - end - - context "against no matching regexes" do - it "is false" do - array = %w[FOO BAR] - attributes = {first: /baz/i, last: /qux/i} - partial = new_partial(array) - matcher = Spectator::Matchers::AttributesMatcher.new(attributes) - matcher.match?(partial).should be_false - end - end - - context "against equal and matching type and regex" do - it "is true" do + describe "#values" do + it "contains a key for each expected attribute" do array = [:a, 42, "FOO"] attributes = {first: Symbol, last: /foo/i, size: 3} partial = new_partial(array) matcher = Spectator::Matchers::AttributesMatcher.new(attributes) - matcher.match?(partial).should be_true + match_data = matcher.match(partial) + values = match_data.values + values.has_key?(:"expected first").should be_true + values.has_key?(:"expected last").should be_true + values.has_key?(:"expected size").should be_true + end + + it "contains a key for each actual value" do + array = [:a, 42, "FOO"] + attributes = {first: Symbol, last: /foo/i, size: 3} + partial = new_partial(array) + matcher = Spectator::Matchers::AttributesMatcher.new(attributes) + match_data = matcher.match(partial) + values = match_data.values + values.has_key?(:"actual first").should be_true + values.has_key?(:"actual last").should be_true + values.has_key?(:"actual size").should be_true + end + + it "has the expected values" do + array = [:a, 42, "FOO"] + attributes = {first: Symbol, last: /foo/i, size: 3} + partial = new_partial(array) + matcher = Spectator::Matchers::AttributesMatcher.new(attributes) + match_data = matcher.match(partial) + values = match_data.values + values[:"expected first"].should eq(attributes[:first]) + values[:"expected last"].should eq(attributes[:last]) + values[:"expected size"].should eq(attributes[:size]) + end + + it "has the actual values" do + array = [:a, 42, "FOO"] + attributes = {first: Symbol, last: /foo/i, size: 3} + partial = new_partial(array) + matcher = Spectator::Matchers::AttributesMatcher.new(attributes) + match_data = matcher.match(partial) + values = match_data.values + values[:"actual first"].should eq(array.first) + values[:"actual last"].should eq(array.last) + values[:"actual size"].should eq(array.size) + end + end + + describe "#message" do + it "contains the actual label" do + value = "foobar" + attributes = {size: 6} + label = "everything" + partial = new_partial(value, label) + matcher = Spectator::Matchers::AttributesMatcher.new(attributes) + match_data = matcher.match(partial) + match_data.message.should contain(label) + end + + it "contains the expected label" do + value = "foobar" + attributes = {size: 6} + label = "everything" + partial = new_partial(value) + matcher = Spectator::Matchers::AttributesMatcher.new(attributes, label) + match_data = matcher.match(partial) + match_data.message.should contain(label) + end + + context "when expected label is omitted" do + it "contains stringified form of expected value" do + value = "foobar" + attributes = {size: 6} + partial = new_partial(value) + matcher = Spectator::Matchers::AttributesMatcher.new(attributes) + match_data = matcher.match(partial) + match_data.message.should contain(attributes.to_s) + end + end + end + + describe "#negated_message" do + it "contains the actual label" do + value = "foobar" + attributes = {size: 6} + label = "everything" + partial = new_partial(value, label) + matcher = Spectator::Matchers::AttributesMatcher.new(attributes) + match_data = matcher.match(partial) + match_data.negated_message.should contain(label) + end + + it "contains the expected label" do + value = "foobar" + attributes = {size: 6} + label = "everything" + partial = new_partial(value) + matcher = Spectator::Matchers::AttributesMatcher.new(attributes, label) + match_data = matcher.match(partial) + match_data.negated_message.should contain(label) + end + + context "when expected label is omitted" do + it "contains stringified form of expected value" do + value = "foobar" + attributes = {size: 6} + partial = new_partial(value) + matcher = Spectator::Matchers::AttributesMatcher.new(attributes) + match_data = matcher.match(partial) + match_data.negated_message.should contain(attributes.to_s) + end end end end end - - describe "#message" do - it "contains the actual label" do - value = "foobar" - attributes = {size: 6} - label = "everything" - partial = new_partial(value, label) - matcher = Spectator::Matchers::AttributesMatcher.new(attributes) - matcher.message(partial).should contain(label) - end - - it "contains the expected label" do - value = "foobar" - attributes = {size: 6} - label = "everything" - partial = new_partial(value) - matcher = Spectator::Matchers::AttributesMatcher.new(attributes, label) - matcher.message(partial).should contain(label) - end - - context "when expected label is omitted" do - it "contains stringified form of expected value" do - value = "foobar" - attributes = {size: 6} - partial = new_partial(value) - matcher = Spectator::Matchers::AttributesMatcher.new(attributes) - matcher.message(partial).should contain(attributes.to_s) - end - end - end - - describe "#negated_message" do - it "contains the actual label" do - value = "foobar" - attributes = {size: 6} - label = "everything" - partial = new_partial(value, label) - matcher = Spectator::Matchers::AttributesMatcher.new(attributes) - matcher.negated_message(partial).should contain(label) - end - - it "contains the expected label" do - value = "foobar" - attributes = {size: 6} - label = "everything" - partial = new_partial(value) - matcher = Spectator::Matchers::AttributesMatcher.new(attributes, label) - matcher.negated_message(partial).should contain(label) - end - - context "when expected label is omitted" do - it "contains stringified form of expected value" do - value = "foobar" - attributes = {size: 6} - partial = new_partial(value) - matcher = Spectator::Matchers::AttributesMatcher.new(attributes) - matcher.negated_message(partial).should contain(attributes.to_s) - end - end - end end diff --git a/src/spectator/matchers/attributes_matcher.cr b/src/spectator/matchers/attributes_matcher.cr index d258785..2b798fa 100644 --- a/src/spectator/matchers/attributes_matcher.cr +++ b/src/spectator/matchers/attributes_matcher.cr @@ -6,13 +6,10 @@ module Spectator::Matchers # The `ExpectedType` type param should be a `NamedTuple`. struct AttributesMatcher(ExpectedType) < ValueMatcher(ExpectedType) # Determines whether the matcher is satisfied with the value given to it. - # True is returned if the match was successful, false otherwise. - def match?(partial) - actual = partial.actual - + private def match?(actual) # Test each attribute and immediately return false if a comparison fails. {% for attribute in ExpectedType.keys %} - return false unless expected[{{attribute.symbolize}}] === actual.{{attribute}} + return false unless expected[{{attribute.symbolize}}] === actual[{{attribute.symbolize}}] {% end %} # All checks passed if this point is reached. @@ -21,20 +18,55 @@ module Spectator::Matchers # Determines whether the matcher is satisfied with the partial given to it. # `MatchData` is returned that contains information about the match. - def match(partial) : MatchData - raise NotImplementedError.new("#match") + def match(partial) + actual_values = snapshot_values(partial.actual) + values = ExpectedActual.new(expected, label, actual_values, partial.label) + MatchData.new(match?(actual_values), values) end - # Describes the condition that satisfies the matcher. - # This is informational and displayed to the end-user. - def message(partial) - "Expected #{partial.label} to have #{label}" + # Captures all of the actual values. + # A `NamedTuple` is returned, + # with each key being the attribute. + private def snapshot_values(actual) + {% begin %} + { + {% for attribute in ExpectedType.keys %} + {{attribute}}: actual.{{attribute}}, + {% end %} + } + {% end %} end - # Describes the condition that won't satsify the matcher. - # This is informational and displayed to the end-user. - def negated_message(partial) - "Expected #{partial.label} to not have #{label}" + # Match data specific to this matcher. + private struct MatchData(ExpectedType, ActualType) < MatchData + # Creates the match data. + def initialize(matched, @values : ExpectedActual(ExpectedType, ActualType)) + super(matched) + end + + # Information about the match. + def values + {% begin %} + { + {% for attribute in ExpectedType.keys %} + {{"expected " + attribute.stringify}}: @values.expected[{{attribute.symbolize}}], + {{"actual " + attribute.stringify}}: @values.actual[{{attribute.symbolize}}], + {% end %} + } + {% end %} + end + + # Describes the condition that satisfies the matcher. + # This is informational and displayed to the end-user. + def message + "#{@values.actual_label} has attributes #{@values.expected_label}" + end + + # Describes the condition that won't satsify the matcher. + # This is informational and displayed to the end-user. + def negated_message + "#{@values.actual_label} does not have attributes #{@values.expected_label}" + end end end end