Implement aggregate_failures

This commit is contained in:
Michael Miller 2021-07-31 11:56:53 -06:00
parent 9a97596b84
commit 4c125d98d4
No known key found for this signature in database
GPG Key ID: FB9F12F7C646A4AD
5 changed files with 100 additions and 2 deletions

View File

@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 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)
- Allow named arguments and assignments for `provided` (`given`) block.
- Add `aggregate_failures` to capture and report multiple failed expectations. [#24](https://gitlab.com/arctic-fox/spectator/-/issues/24)
### 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,32 @@
require "../spec_helper"
Spectator.describe Spectator do
describe "aggregate_failures" do
it "captures multiple failed expectations" do
expect do
aggregate_failures do
expect(true).to be_false
expect(false).to be_true
end
end.to raise_error(Spectator::MultipleExpectationsFailed, /2 failures/)
end
it "raises normally for one failed expectation" do
expect do
aggregate_failures do
expect(true).to be_false
expect(true).to be_true
end
end.to raise_error(Spectator::ExpectationFailed)
end
it "doesn't raise when there are no failed expectations" do
expect do
aggregate_failures do
expect(false).to be_false
expect(true).to be_true
end
end.to_not raise_error(Spectator::ExpectationFailed)
end
end
end

View File

@ -162,5 +162,20 @@ module Spectator::DSL
macro is_not(expected)
expect(subject).not_to(eq({{expected}}))
end
# Captures multiple possible failures.
# Aborts after the block completes if there were any failed expectations in the block.
#
# ```
# aggregate_failures do
# expect(true).to be_false
# expect(false).to be_true
# end
# ```
def aggregate_failures
::Spectator::Harness.current.aggregate_failures do
yield
end
end
end
end

View File

@ -2,7 +2,9 @@ require "./error_result"
require "./example_failed"
require "./example_pending"
require "./expectation"
require "./expectation_failed"
require "./mocks"
require "./multiple_expectations_failed"
require "./pass_result"
require "./result"
@ -66,6 +68,7 @@ module Spectator
@deferred = Deque(->).new
@expectations = [] of Expectation
@aggregate : Array(Expectation)? = nil
# Runs test code and produces a result based on the outcome.
# The test code should be called from within the block given to this method.
@ -75,14 +78,20 @@ module Spectator
translate(elapsed + elapsed2, error || error2)
end
def report(expectation : Expectation) : Nil
def report(expectation : Expectation) : Bool
Log.debug { "Reporting expectation #{expectation}" }
@expectations << expectation
# TODO: Move this out of harness, maybe to `Example`.
Example.current.name = expectation.description unless Example.current.name?
raise ExpectationFailed.new(expectation, expectation.failure_message) if expectation.failed?
if expectation.failed?
raise ExpectationFailed.new(expectation, expectation.failure_message) unless (aggregate = @aggregate)
aggregate << expectation
false
else
true
end
end
# Stores a block of code to be executed later.
@ -91,6 +100,31 @@ module Spectator
@deferred << block
end
def aggregate_failures
previous = @aggregate
@aggregate = aggregate = [] of Expectation
begin
yield.tap do
# If there's an nested aggregate (for some reason), allow the top-level one to handle things.
check_aggregate(aggregate) unless previous
end
ensure
@aggregate = previous
end
end
private def check_aggregate(aggregate)
failures = aggregate.select(&.failed?)
case failures.size
when 0 then return
when 1
expectation = failures.first
raise ExpectationFailed.new(expectation, expectation.failure_message)
else
raise MultipleExpectationsFailed.new(failures, "Got #{failures.size} failures from failure aggregation block")
end
end
# Yields to run the test code and returns information about the outcome.
# Returns a tuple with the elapsed time and an error if one occurred (otherwise nil).
private def capture : Tuple(Time::Span, Exception?)

View File

@ -0,0 +1,16 @@
require "./example_failed"
require "./expectation"
module Spectator
# Exception that indicates more than one expectation from a test failed.
# When raised within a test, the test should abort.
class MultipleExpectationsFailed < ExampleFailed
# Expectations that failed.
getter expectations : Array(Expectation)
# Creates the exception.
def initialize(@expectations : Array(Expectation), message : String? = nil, cause : Exception? = nil)
super(nil, message, cause)
end
end
end