From 3e1ee7eb6d225756ae274002b175cf959b2b1a42 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 14 Jul 2019 09:39:27 -0600 Subject: [PATCH] Initial code for basic change matcher --- spec/matchers/change_matcher_spec.cr | 81 ++++++++++++++++++++++++ src/spectator/dsl/matcher_dsl.cr | 32 ++++++++++ src/spectator/matchers/change_matcher.cr | 60 ++++++++++++++++++ 3 files changed, 173 insertions(+) create mode 100644 spec/matchers/change_matcher_spec.cr create mode 100644 src/spectator/matchers/change_matcher.cr diff --git a/spec/matchers/change_matcher_spec.cr b/spec/matchers/change_matcher_spec.cr new file mode 100644 index 0000000..a101537 --- /dev/null +++ b/spec/matchers/change_matcher_spec.cr @@ -0,0 +1,81 @@ +require "../spec_helper" + +describe Spectator::Matchers::ChangeMatcher do + describe "#match" do + context "returned MatchData" do + context "with changing expression" do + describe "#matched?" do + it "is true" do + i = 0 + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeMatcher.new { i } + match_data = matcher.match(partial) + match_data.matched?.should be_true + end + end + + describe "#values" do + context "before" do + it "is the initial value" do + i = 0 + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeMatcher.new { i } + match_data = matcher.match(partial) + match_data_value_with_key(match_data.values, :before).value.should eq(0) + end + end + + context "after" do + it "is the resulting value" do + i = 0 + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeMatcher.new { i } + match_data = matcher.match(partial) + match_data_value_with_key(match_data.values, :after).value.should eq(5) + end + end + end + + describe "#message" do + it "contains the action label" do + i = 0 + label = "ACTION" + partial = new_block_partial(label) { i += 5 } + matcher = Spectator::Matchers::ChangeMatcher.new { i } + match_data = matcher.match(partial) + match_data.message.should contain(label) + end + + it "contains the expression label" do + i = 0 + label = "EXPRESSION" + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeMatcher.new(label) { i } + match_data = matcher.match(partial) + match_data.message.should contain(label) + end + end + + describe "#negated_message" do + it "contains the action label" do + i = 0 + label = "ACTION" + partial = new_block_partial(label) { i += 5 } + matcher = Spectator::Matchers::ChangeMatcher.new { i } + match_data = matcher.match(partial) + match_data.negated_message.should contain(label) + end + + it "contains the expression label" do + i = 0 + label = "EXPRESSION" + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeMatcher.new(label) { i } + match_data = matcher.match(partial) + match_data.negated_message.should contain(label) + end + end + end + end + end +end diff --git a/src/spectator/dsl/matcher_dsl.cr b/src/spectator/dsl/matcher_dsl.cr index 18b30ab..77c54dd 100644 --- a/src/spectator/dsl/matcher_dsl.cr +++ b/src/spectator/dsl/matcher_dsl.cr @@ -506,6 +506,38 @@ module Spectator::DSL ::Spectator::Matchers::AttributesMatcher.new(::Spectator::TestValue.new({{expected}}, {{expected.double_splat.stringify}})) end + # Indicates that some expression's value should change after taking an action. + # + # Examples: + # ``` + # i = 0 + # expect { i += 1 }.to change { i } + # expect { i += 0 }.to_not change { i } + # ``` + # + # ``` + # i = 0 + # expect { i += 5 }.to change { i }.from(0).to(5) + # ``` + # + # ``` + # i = 0 + # expect { i += 5 }.to change { i }.to(5) + # ``` + # + # ``` + # i = 0 + # expect { i += 5 }.to change { i }.from(0) + # ``` + # + # ``` + # i = 0 + # expect { i += 42 }.to change { i }.by(42) + # ``` + macro change(&expression) + ::Spectator::Matchers::ChangeMatcher.new("`" + {{expression.body.stringify}} + "`") {{expression}} + end + # Indicates that some block should raise an error. # # Examples: diff --git a/src/spectator/matchers/change_matcher.cr b/src/spectator/matchers/change_matcher.cr new file mode 100644 index 0000000..f8bcb8f --- /dev/null +++ b/src/spectator/matchers/change_matcher.cr @@ -0,0 +1,60 @@ +require "./value_matcher" + +module Spectator::Matchers + # Matcher that tests whether an expression changed. + struct ChangeMatcher(ExpressionType) < ValueMatcher(ExpressionType) + # Determines whether the matcher is satisfied with the value given to it. + private def match?(after) + expected != after + end + + # Determines whether the matcher is satisfied with the partial given to it. + # `MatchData` is returned that contains information about the match. + def match(partial) + partial.actual # Invoke action that might change the expression's value. + after = @expression.call # Retrieve the expression's value. + MatchData.new(match?(after), expected, after, partial.label, label) + end + + # Creates a new change matcher with a custom label. + def initialize(label : String, &expression : -> ExpressionType) + super(yield, label) + @expression = expression + end + + # Creates a new change matcher. + def initialize(&expression : -> ExpressionType) + super(yield, expression.to_s) + @expression = expression + end + + # Match data specific to this matcher. + private struct MatchData(ExpressionType) < MatchData + # Creates the match data. + def initialize(matched, @before : ExpressionType, @after : ExpressionType, + @action_label : String, @expression_label : String) + super(matched) + end + + # Information about the match. + def named_tuple + { + before: @before, + after: @after + } + end + + # Describes the condition that satisfies the matcher. + # This is informational and displayed to the end-user. + def message + "#{@action_label} changes #{@expression_label}" + end + + # Describes the condition that won't satsify the matcher. + # This is informational and displayed to the end-user. + def negated_message + "#{@action_label} does not change #{@expression_label}" + end + end + end +end