-
-
Notifications
You must be signed in to change notification settings - Fork 397
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature idea: aggregate_failures
#733
Comments
I'm not sure this will solve the issue because people want multiple failures, not one massive failure, but it is a good compromise and I personally like it as a starter. I wonder if we could somehow report (we probably can using the reporter), multiple failures from one test (so it's 1 pass, or x failures)... This gives you the advantage of having the compact output when passing, but having individual failures you can fix... 👍 on the metadata option too... |
I don't necessarily think people want multiple failures, they just want to see all the failure messages. I would use this. I've wasted more time than I care to admit flipping between whether I do the first or second of the current ways to do this currently. Implementation wise the only part that scares me a little is the thread local, though I don't have any concrete issues with it. I like the metadata option, I'd be leaning towards only supporting that. (And maybe even making it the default eventually?) I don't see a strong reason (beyond esoteric hypotheticals) for either using multiple blocks, nor having some expects inside and some outside. At least, I'd start with metadata option first and then only add block if we found it lacking. Could you instance eval the block with a redefined |
As @xaviershay said, I think people just want to see all the failure messages. Ultimately one example is one example and can only count as one failure in the formatter summary. This feature would never change that. We could, however, add some additional metadata to
Thread locals always feel a bit like a hack but it's the best solution I could come up with, and making it thread local seems important in light of our plans to support a multi-threaded runner at some point (e.g. rspec/rspec-core#1254).
Problem is, rspec-expectations has no concept of example metadata because it has no concept of examples. Metadata is purely an rspec-core concept. Given that we intend for rspec-expectations to be usable on its own (or in a context like cucumber), it needs to expose an API for this, and then I can also see cases where it would be useful to only apply this to part of an example. For example: context "when a record has been saved" do
it "returns a response with particular properties" do
expect(MyModel.create(valid_attributes)).to be true
get '/some/endpoint'
aggregate_failures do
expect(response).to foo
expect(response).to bar
end
end
end In this example, the first expectation is validating an assumption/pre-condition that the record got saved properly. (In practice you'd probably use
Maybe, although again I can see cases where you may not want it, like the case above. It's something we can consider down the line if we see this being used in most situations by users.
It's not just as weird. It's far, far weirder. I briefly thought about taking that route, but consider:
So one big question is: what do we call this? My best ideas so far are:
All those names work OK for me but none is obviously the best name. Any better ideas (or does one of those standout as best?) A couple other issues to consider...
RSpec.describe "Around hooks" do
around do |ex|
puts "Before outer around"
ex.run
puts "After outer around"
end
context "when an error is raised in an example" do
it "still runs the after logic of the around hook" do
raise "boom"
end
end
context "when an error is raised in the after logic of an around hook" do
around do |ex|
puts "Before inner around"
ex.run
raise "boom"
end
it "aborts the after logic of other around hooks" do
end
end
end Here's the output of
Notice that This matters here because the metadata integration we have discussed with trigger this issue, since it would raise an error from the after portion of an |
I too would use this. I've run in to a case where I'm driving long-running integration tests with Rspec and would rather all my expect() blocks execute than failing on the first missed expectation because setup for the tests take a long time and I want a full list of problems that need attention while only incurring one setup. |
I would definitely love this feature, both metadata and block approach |
I'm hugely 👍 on this feature. |
I plan to tackle this in the next week or so and then I'm hoping to ship RSpec 3.3 after that. |
👍 for this feature. While I 💙 💙 composable + compound matchers, there are times they do not work cleanly to define a "behavior". At that point I must decide how if it's worth writing a custom matcher or not. This feature would allow me to skip those times where I do not want or need a custom matcher but still want to group a set of expectations.
I would really like to see the block form as well. This would come in handy for "workflow" based specs. Often I care about the set of expectations at a particular point in the flow, not every expectation throughout the entire flow. Would it be possible to assign the block form an additional description? aggregate_failures "this explains some additional context" do
expect(result).to blah
expect(result).to blah_blah
expect(result).to have_other_interesting_properties
end
Personally I like I haven't spent too much time thinking about names but some others:
Though that may be a bit too much
Personally, I would expect the exception to terminate the spec and be reported as the failure. For example, I can see someone writing: aggregate_failures do
expect(obj.foo).to blah
bar.do_something(foo) # raises exception
expect(obj.some_thing).to blah_blah
expect(obj.foo).to have_other_interesting_properties
end In the above contrived example, the last two expectations will fail because
I don't have any real ideas. I would expect that any spy expectation to be verified at the end of the block. Likewise, any mock defined in the block would be verified at that point. |
Yes, although I would only want to support that if the additional description was used in some fashion. Do you have something in mind for how we would use that? If we're not going to use it for anything, the user can just use ruby comments to add additional context -- no need for us to support an argument we don't do anything with. |
I was thinking as part of the aggregated message. Did you have an idea what an aggregate failure message would look like? |
I haven't given it too much thought, but something like:
I suppose the string could be used in place of the "at
Although, perhaps we should still include the block source location. |
I think it should still include the source location :) |
👍 for that output. I agree with Jon we should always include the source location. |
One potential issue with that output: when it is printed in the end-of-run list of failures, it could get confusing with the fact that the failures are numbered with |
This is intended to support the new `aggregate_failures` feature discussed in rspec/rspec-expectations#733. I put the `failure_notifier` here so that rspec-mocks can use it as well -- that way, when we are aggregating failures, we can aggregate rspec-mocks failures as well.
This is intended to support the new `aggregate_failures` feature discussed in rspec/rspec-expectations#733. I put the `failure_notifier` here so that rspec-mocks can use it as well -- that way, when we are aggregating failures, we can aggregate rspec-mocks failures as well.
This is intended to support the new `aggregate_failures` feature discussed in rspec/rspec-expectations#733. I put the `failure_notifier` here so that rspec-mocks can use it as well -- that way, when we are aggregating failures, we can aggregate rspec-mocks failures as well.
Nested enumeration? Failures:
|
So I've started on this in #776. So far, here's the output we're getting: RSpec.describe "An aggregated failure" do
it "lists each of the individual failures in the failure output" do
aggregate_failures "testing equality" do
expect(1).to eq(2)
expect(2).to eq(3)
expect(3).to eq(4)
end
end
end
Pretty decent, but what I'd like is this:
The latter includes the line of source code that rspec-core's failure formatting adds as well as the stacktrace for each failure. I haven't yet figured out the best way to get that but the error being raised exposes the data about all the failures so that rspec-core can apply its formatting logic to it. I'm thinking we may actually want to cut the "Got 3 failures from the failure aggregation block" line, at least from the rspec-core failure output. It's redundant -- the line above it shows the Thoughts? |
Yep, cut it :) |
So I've bee making progress on this. The formatting is honestly the hardest part! I've got some aweful code that needs refactoring that produces output like this:
A few things to note:
Thoughts? I definitely think this is improved from what I put above, but there is one thing that bugs me about it....the aggregation block stacktrace is in between the
Is that what we want? |
That last output looks good to me, but are we printing the entire backtrace or just the one liner version? I'm thinking theres a strong case for only printing a truncated "this is where the error" came from rather than a full backtrace as these often get rather large and will just swamp the output. |
My plan is to print the entire backtrace of the We could truncate the def test_endpoint_with(version)
get "/endpoint?v=#{version}"
aggregate_failures "checking response" do
# expectations here
end
end
it 'tests endpoint x' do
test_endpoint_with 1
test_endpoint_with 2
end If we truncated to a single frame, you'd have no way to know whether the failure happened for the first |
When you've got multiple independent expectations to make about a particular result, there are generally two routes people take:
Nether is perfectly ideal -- the latter gives you failure output for each expectation that failed, whereas the former runs faster, particularly if
MyThing.do_it
is slow. People ask if RSpec supports a better solution for this from time to time (the most recent example is on the mailing list).One solution is available in rspec-given: it's And API. That's not going to work in vanilla RSpec, of course, but it got me thinking about this, and I've come up with an idea that I think would work quite well: a new block API like
aggregate_failures
. Here's how it would work:The idea is that you can wrap any set of expectations with it, and it aggregates all the expectation failures that occur in the block, and, if any occur, raises a single exception at the end of the block containing all the failure messages.
Implementation wise, I've thought of a super simple way to do this: change fail_with so that rather than calling
raise
directly, it instead invokesRSpec::Expectations.configuration.failure_notifier.call(failure)
. We'd implementfailure_notifier
so that it uses a thread local for storage and defaults toKernel.raise
.aggregate_failures
would then simply change thefailure_notifier
for the current thread to an alternate implementation that aggregates the failures rather than raising immediately, yield, change it back, and then raise with all the failure if there are any.I'm also thinking that we could automatically make this available via metadata as well:
...which would use an
around
hook to wrap the example inaggregate_failures
.I'd like to hear from others about this:
aggregate_failures
the right name for it? (It's the first OK name I thought of but I don't love it). Other ideas I have arecollect_failures
anddelay_failures
. I don't love any of my ideas, though./cc @rspec/rspec
The text was updated successfully, but these errors were encountered: