From e8413db33f8f9f3d44ac2729431a6d3e19449194 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 17 Jul 2021 17:42:25 -0600 Subject: [PATCH] Support custom messages for failed expectations Fixes https://gitlab.com/arctic-fox/spectator/-/issues/28 --- CHANGELOG.md | 1 + spec/custom_message_spec.cr | 31 +++++++++++++++++++ src/spectator/expectation.cr | 60 +++++++++++++++++++++--------------- src/spectator/should.cr | 46 +++++++++++++++------------ 4 files changed, 94 insertions(+), 44 deletions(-) create mode 100644 spec/custom_message_spec.cr diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f70cf5..63ed0f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `before_suite` and `after_suite` hooks. [#21](https://gitlab.com/arctic-fox/spectator/-/issues/21) - Support defining hooks in `Spectator.configure` block. [#21](https://gitlab.com/arctic-fox/spectator/-/issues/21) - Examples with failures or skipped during execution will report the location of that result. [#57](https://gitlab.com/arctic-fox/spectator/-/issues/57) +- Support custom messages for failed expectations. [#28](https://gitlab.com/arctic-fox/spectator/-/issues/28) ### Changed - `around_each` hooks wrap `before_all` and `after_all` hooks. [#12](https://github.com/icy-arctic-fox/spectator/issues/12) diff --git a/spec/custom_message_spec.cr b/spec/custom_message_spec.cr new file mode 100644 index 0000000..64ca32b --- /dev/null +++ b/spec/custom_message_spec.cr @@ -0,0 +1,31 @@ +require "./spec_helper" + +Spectator.describe Spectator do + it "supports custom expectation messages" do + expect do + expect(false).to be_true, "paradox!" + end.to raise_error(Spectator::ExampleFailed, "paradox!") + end + + it "supports custom expectation messages with a proc" do + count = 0 + expect do + expect(false).to be_true, ->{ count += 1; "Failed #{count} times" } + end.to raise_error(Spectator::ExampleFailed, "Failed 1 times") + end + + context "not_to" do + it "supports custom expectation messages" do + expect do + expect(true).not_to be_true, "paradox!" + end.to raise_error(Spectator::ExampleFailed, "paradox!") + end + + it "supports custom expectation messages with a proc" do + count = 0 + expect do + expect(true).not_to be_true, ->{ count += 1; "Failed #{count} times" } + end.to raise_error(Spectator::ExampleFailed, "Failed 1 times") + end + end +end diff --git a/src/spectator/expectation.cr b/src/spectator/expectation.cr index cc376b6..adb2abb 100644 --- a/src/spectator/expectation.cr +++ b/src/spectator/expectation.cr @@ -24,7 +24,13 @@ module Spectator # If nil, then the match was successful. def failure_message? - @match_data.as?(Matchers::FailedMatchData).try(&.failure_message) + return unless match_data = @match_data.as?(Matchers::FailedMatchData) + + case message = @message + when String then message + when Proc(String) then @message = message.call # Cache result of call. + else match_data.failure_message + end end # Description of why the match failed. @@ -50,7 +56,9 @@ module Spectator # Creates the expectation. # The *match_data* comes from the result of calling `Matcher#match`. # The *location* is the location of the expectation in source code, if available. - def initialize(@match_data : Matchers::MatchData, @location : Location? = nil) + # A custom *message* can be used in case of a failure. + def initialize(@match_data : Matchers::MatchData, @location : Location? = nil, + @message : String? | Proc(String) = nil) end # Creates the JSON representation of the expectation. @@ -92,9 +100,10 @@ module Spectator end # Asserts that some criteria defined by the matcher is satisfied. - def to(matcher) : Nil + # Allows a custom message to be used. + def to(matcher, message = nil) : Nil match_data = matcher.match(@expression) - report(match_data) + report(match_data, message) end def to(stub : Mocks::MethodStub) : Nil @@ -110,9 +119,16 @@ module Spectator # Asserts that some criteria defined by the matcher is not satisfied. # This is effectively the opposite of `#to`. - def to_not(matcher) : Nil + # Allows a custom message to be used. + def to_not(matcher, message = nil) : Nil match_data = matcher.negated_match(@expression) - report(match_data) + report(match_data, message) + end + + # :ditto: + @[AlwaysInline] + def not_to(matcher, message = nil) : Nil + to_not(matcher, message) end def to_not(stub : Mocks::MethodStub) : Nil @@ -125,16 +141,11 @@ module Spectator stubs.each { |stub| to_not(stub) } end - # :ditto: - @[AlwaysInline] - def not_to(matcher) : Nil - to_not(matcher) - end - # Asserts that some criteria defined by the matcher is eventually satisfied. # The expectation is checked after the example finishes and all hooks have run. - def to_eventually(matcher) : Nil - Harness.current.defer { to(matcher) } + # Allows a custom message to be used. + def to_eventually(matcher, message = nil) : Nil + Harness.current.defer { to(matcher, message) } end def to_eventually(stub : Mocks::MethodStub) : Nil @@ -147,8 +158,15 @@ module Spectator # Asserts that some criteria defined by the matcher is never satisfied. # The expectation is checked after the example finishes and all hooks have run. - def to_never(matcher) : Nil - Harness.current.defer { to_not(matcher) } + # Allows a custom message to be used. + def to_never(matcher, message = nil) : Nil + Harness.current.defer { to_not(matcher, message) } + end + + # :ditto: + @[AlwaysInline] + def never_to(matcher, message = nil) : Nil + to_never(matcher, message) end def to_never(stub : Mocks::MethodStub) : Nil @@ -159,15 +177,9 @@ module Spectator to_not(stub) end - # :ditto: - @[AlwaysInline] - def never_to(matcher) : Nil - to_never(matcher) - end - # Reports an expectation to the current harness. - private def report(match_data : Matchers::MatchData) - expectation = Expectation.new(match_data, @location) + private def report(match_data : Matchers::MatchData, message : String? | Proc(String) = nil) + expectation = Expectation.new(match_data, @location, message) Harness.current.report(expectation) end end diff --git a/src/spectator/should.cr b/src/spectator/should.cr index 7ebcf84..71e5574 100644 --- a/src/spectator/should.cr +++ b/src/spectator/should.cr @@ -9,6 +9,12 @@ class Object # end # ``` # + # An optional message can be used in case the expectation fails. + # It can be a string or proc returning a string. + # ``` + # subject.should_not be_nil, "Shouldn't be nil" + # ``` + # # NOTE: By default, the should-syntax is disabled. # The expect-syntax is preferred, # since it doesn't [monkey-patch](https://en.wikipedia.org/wiki/Monkey_patch) all objects. @@ -16,69 +22,69 @@ class Object # ``` # require "spectator/should" # ``` - def should(matcher) + def should(matcher, message = nil) actual = ::Spectator::Value.new(self) match_data = matcher.match(actual) - expectation = ::Spectator::Expectation.new(match_data) + expectation = ::Spectator::Expectation.new(match_data, message: message) ::Spectator::Harness.current.report(expectation) end # Works the same as `#should` except the condition is inverted. # When `#should` succeeds, this method will fail, and vice-versa. - def should_not(matcher) + def should_not(matcher, message = nil) actual = ::Spectator::Value.new(self) match_data = matcher.negated_match(actual) - expectation = ::Spectator::Expectation.new(match_data) + expectation = ::Spectator::Expectation.new(match_data, message: message) ::Spectator::Harness.current.report(expectation) end # Works the same as `#should` except that the condition check is postphoned. # The expectation is checked after the example finishes and all hooks have run. - def should_eventually(matcher) - ::Spectator::Harness.current.defer { should(matcher) } + def should_eventually(matcher, message = nil) + ::Spectator::Harness.current.defer { should(matcher, message) } end # Works the same as `#should_not` except that the condition check is postphoned. # The expectation is checked after the example finishes and all hooks have run. - def should_never(matcher) - ::Spectator::Harness.current.defer { should_not(matcher) } + def should_never(matcher, message = nil) + ::Spectator::Harness.current.defer { should_not(matcher, message) } end end struct Proc(*T, R) # Extension method to create an expectation for a block of code (proc). # Depending on the matcher, the proc may be executed multiple times. - def should(matcher) + def should(matcher, message = nil) actual = ::Spectator::Block.new(self) match_data = matcher.match(actual) - expectation = ::Spectator::Expectation.new(match_data) + expectation = ::Spectator::Expectation.new(match_data, message: message) ::Spectator::Harness.current.report(expectation) end # Works the same as `#should` except the condition is inverted. # When `#should` succeeds, this method will fail, and vice-versa. - def should_not(matcher) + def should_not(matcher, message = nil) actual = ::Spectator::Block.new(self) match_data = matcher.negated_match(actual) - expectation = ::Spectator::Expectation.new(match_data) + expectation = ::Spectator::Expectation.new(match_data, message: message) ::Spectator::Harness.current.report(expectation) end end module Spectator::DSL::Expectations - macro should(matcher) - expect(subject).to({{matcher}}) + macro should(*args) + expect(subject).to({{args.splat}}) end - macro should_not(matcher) - expect(subject).to_not({{matcher}}) + macro should_not(*args) + expect(subject).to_not({{args.splat}}) end - macro should_eventually(matcher) - expect(subject).to_eventually({{matcher}}) + macro should_eventually(*args) + expect(subject).to_eventually({{args.splat}}) end - macro should_never(matcher) - expect(subject).to_never({{matcher}}) + macro should_never(*args) + expect(subject).to_never({{args.splat}}) end end