nunit: Regression in 3.6.0 when catching AssertionException
@pdpurcell commented on Thu Feb 16 2017
In version 3.5.0, catching a failed assertion would allow the test to continue and the test would then pass. Version 3.6.0 continues running the code, but the final state will be set to failed. Is this a regression? I see no mention of this sort of behaviour change in the release notes.
I have attached an example solution that contains two projects that have identical tests; one using 3.5.0 and one using 3.6.0.
eg:
try
{
Assert.AreEqual(0, 1);
}
catch(AssertionException)
{
// The failed assertion is caught but in 3.6.0 the test still fails
}
@CharliePoole commented on Thu Feb 16 2017
The bad news is that this has never actually been supported even though it’s a technique promulgated on many blogs and forums. That is…
NUnit makes no guarantee that failures cause an AssertionException that catching the exception will prevent the failure from happening. This is certainly not a guarantee in NUnit 3 and - to my memory - never was in NUnit V2 either.
When this technique has come up, I have frequently tried to warn people off using it. There are lots of reasons to avoid it:
-
It implies that your test knows something about the framework that is executing it, which is a serious violation of encapsulation - that is, unless you are actually testing the internals of framework itself.
-
There are usually supported ways to accomplish whatever you are trying to do, without the try / catch.
-
Where there is an unsupported need, it makes more sense to request a feature, which will be supported now and in the future.
-
It’s an implementation detail of NUnit, which could change at any time.
Of course, the last point has come true in the latest release.
In NUnit 3.6, the details of the implementation have changed. We still use an exception to terminate execution of your test. It’s the only way we have to unwind any nested calls you have made. However, we no longer use the exception to report that the test failed. That info is recorded before the exception is thrown. This allows us to use a consistent approach for single assertion failures, multiple assertion failures and warnings.
As you say, there is no mention of this in the release notes. That’s because we felt that there was no need to document an internal change that should never be used by users. For the same reason, we didn’t list this as a breaking change. From some of the feedback, it’s clear that there are users who did not consider this a detail of implementation and relied on it. There are already several issues.
Most of the issues - like yours - illustrate the “problem” without motivating the need to be able to intercept a failure. In each case, I have asked the submitter of the issue to give a motivating example that would help us meet the need. IOW we need to know why you want to be able to prevent a failure by catching the AssertionException. In the long term, we may want to add features that make it unnecessary for you to do such a thing!
Since you’re the fourth or fifth person to raise the issue, it is clear that we need to do something, in spite of my continued belief that you should not really be relying on how NUnit is implemented. I’ll close this comment and start a fresh one with a proposal to partially role back the change.
@CharliePoole commented on Thu Feb 16 2017
@nunit/framework-team @nunit/core-team As stated in my first comment here and on several other issues, I continue to believe that folks who rely on AssertionException and particularly on catching it are making a mistake. “NUnit implements failures via an assertion exception and you can stop the failure by catching it” is not a sentence in our documentation and not part of the API we provide to programmers.
Several people have pointed out reasons why it should work, however:
- Most other frameworks work that way
- It has worked in the past
- Exceptions thrown is part of the API of many methods
I could argue against all of those points, but why bother? It seems that a number of users are affected and I think there’s a change that can be made. Here’s what I propose…
-
Stop creating the failure message in the result before throwing an AssertionException outside of a Multiple Assert block.
-
Continue to create the failure message without throwing when inside a Multiple Assert block.
-
Upon exit from a MultipleAssertBlock in which failures have occured, throw a (new) MultipleAssertionException rather than an AssertionException, so that the framework knows that one or more failures have already been recorded and need to be reported.
-
Continue to handle warnings as we do, recording them without throwing an exception.
This will ensure that where we do throw an AssertionException it works exactly as before. Those places where we don’t throw one are new features so no compatibility issue arises.
I think this is critical enough to warrant a hot fix release. Please let me know if you agree with that as well as with the fix itself. I can do the fix myself pretty quickly once we agree on it.
About this issue
- Original URL
- State: closed
- Created 7 years ago
- Comments: 23 (20 by maintainers)
Commits related to this issue
- Don't reinvent `CollectionAssert.AreEquivalent` ...especially when the test that verifies the reinvention's proper be- havior relies on catching `AssertionException`, which is something a test should... — committed to stakx/Castle.Core by stakx 5 years ago
- Don't reinvent `CollectionAssert.AreEquivalent` ...especially when the test that verifies the reinvention's proper be- havior relies on catching `AssertionException`, which is something a test should... — committed to stakx/Castle.Core by stakx 5 years ago
- Don't reinvent `CollectionAssert.AreEquivalent` ...especially when the test that verifies the reinvention's proper be- havior relies on catching `AssertionException`, which is something a test should... — committed to stakx/Castle.Core by stakx 5 years ago
- Don't reinvent `CollectionAssert.AreEquivalent` ...especially when the test that verifies the reinvention's proper be- havior relies on catching `AssertionException`, which is something a test should... — committed to stakx/Castle.Core by stakx 5 years ago
@jnm2 Agree for most libraries. But this is an exception you are not supposed to catch.
In any case, one is usually advised to only catch exceptions that you can actually handle, which is not the case here.
However, we can still try to minimize the surprise for users.
@ChrisMaddock I implemented it as I did to avoid introducing a new exception type. In retrospect, the new type should make things clearer.
I may be mixing this up with the ability to directly call test methods, which is somewhat related. In any case, an earlier PR “fixed” this for 3.6 and then I think my unrelated PR #2700 broke it for 3.10.
In any case, whichever way it goes, I agree the documentation should say something about it. I guess one of:
“Catching an assertion exception will prevent the test from failing.” OR “Catching an AssertionException will not prevent the test from failing.” OR “Do not try to catch an AssertionException in your tests as it causes undefined behavior.”
@appel1 The mechanisms we use to report failures are such a fundamental part of NUni that I’d be hesitant to complicate things by a setting that causes it to work in two different ways. The solution in the PR is to revert to the “old” way for simple failures anuses the new mechanism for new features. As we all know, people shouldn’t be catching exceptions indiscriminately.
For NUnit’s own exceptions, I guess we need a warning in the docs.