Support custom messages for failed expectations

Fixes https://gitlab.com/arctic-fox/spectator/-/issues/28
This commit is contained in:
Michael Miller 2021-07-17 17:42:25 -06:00
parent 0c4379c731
commit e8413db33f
No known key found for this signature in database
GPG key ID: FB9F12F7C646A4AD
4 changed files with 94 additions and 44 deletions

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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