diff --git a/src/spectator/matchers/change_matcher.cr b/src/spectator/matchers/change_matcher.cr index d30469c..ad8ca89 100644 --- a/src/spectator/matchers/change_matcher.cr +++ b/src/spectator/matchers/change_matcher.cr @@ -7,6 +7,7 @@ require "./successful_match_data" module Spectator::Matchers # Matcher that tests whether an expression changed. struct ChangeMatcher(ExpressionType) < Matcher + # The expression that is expected to (not) change. private getter expression # Creates a new change matcher. @@ -57,6 +58,21 @@ module Spectator::Matchers ChangeToMatcher.new(@expression, value) end + # Specifies that t he resulting value must be some amount different. + def by(amount : T) forall T + ChangeRelativeMatcher.new(@expression, "by #{amount}") { |before, after| amount == after - before } + end + + # Specifies that the resulting value must be at least some amount different. + def by_at_least(minimum : T) forall T + ChangeRelativeMatcher.new(@expression, "by at least #{minimum}") { |before, after| minimum <= after - before } + end + + # Specifies that the resulting value must be at most some amount different. + def by_at_most(maximum : T) forall T + ChangeRelativeMatcher.new(@expression, "by at most #{maximum}") { |before, after| maximum >= after - before } + end + # Performs the change and reports the before and after values. private def change(actual) before = expression.value # Retrieve the expression's initial value. diff --git a/src/spectator/matchers/change_relative_matcher.cr b/src/spectator/matchers/change_relative_matcher.cr new file mode 100644 index 0000000..adb4cdc --- /dev/null +++ b/src/spectator/matchers/change_relative_matcher.cr @@ -0,0 +1,57 @@ +require "./failed_match_data" +require "./matcher" +require "./successful_match_data" + +module Spectator::Matchers + # Matcher that tests whether an expression changed by an amount. + struct ChangeRelativeMatcher(ExpressionType) < Matcher + # The expression that is expected to (not) change. + private getter expression + + # Creates a new change matcher. + def initialize(@expression : TestBlock(ExpressionType), @relativity : String, + &evaluator : ExpressionType, ExpressionType -> Bool) + @evaluator = evaluator + end + + # 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 + "changes #{expression.label} #{@relativity}" + end + + # Actually performs the test against the expression. + def match(actual : TestExpression(T)) : MatchData forall T + before, after = change(actual) + if before == after + FailedMatchData.new("#{actual.label} did not change #{expression.label}", + before: before.inspect, + after: after.inspect + ) + elsif @evaluator.call(before, after) + SuccessfulMatchData.new + else + FailedMatchData.new("#{actual.label} did not change #{expression.label} #{@relativity}", + before: before.inspect, + after: after.inspect + ) + end + end + + # Performs the test against the expression, but inverted. + # A successful match with `#match` should normally fail for this method, and vice-versa. + def negated_match(actual : TestExpression(T)) : MatchData forall T + {% raise "The `expect { }.to_not change { }.by_...()` syntax is not supported (ambiguous)." %} + end + + # Performs the change and reports the before and after values. + private def change(actual) + before = expression.value # Retrieve the expression's initial value. + actual.value # Invoke action that might change the expression's value. + after = expression.value # Retrieve the expression's value again. + + {before, after} + end + end +end