Problem/Motivation
Spin off from #3268932-49: Add methods to assert status messages to WebAssert
From https://phpunit.readthedocs.io/en/9.5/textui.html:
For each test run, the PHPUnit command-line tool prints one character to indicate progress:
. Printed when the test succeeds.
F Printed when an assertion fails while running the test method.
E Printed when an error occurs while running the test method.
and
PHPUnit distinguishes between failures and errors. A failure is a violated PHPUnit assertion such as a failing assertSame() call. An error is an unexpected exception or a PHP error. Sometimes this distinction proves useful since errors tend to be easier to fix than failures. If you have a big list of problems, it is best to tackle the errors first and see if you have any failures left when they are all fixed.
In current Drupal, WebAssert methods, in case of failure, throw a Mink exception which in PHPUnit are reported as errors and not failures. This is a bit confusing when looking at test run reports because the assertion in these cases are actually checking a condition.
Proposed resolution
Catch Mink's ExpectationException in BrowserTestBase and re-throw a PHPUnit AssertionFailedError instead, ensuring the stack trace remains intact as this is critical to debugging test failures.
Remaining tasks
User interface changes
API changes
Data model changes
Release notes snippet
| Comment | File | Size | Author |
|---|
Issue fork drupal-3271214
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #3
mondrakeChanging WebAssert::assert() is not enough: it will only fit for the assertions called within Drupal's override of the class. When we use methods defined on the parent Mink's WebAssert, the assert() method used is Mink's one, which throws Mink exceptions. So we would need to wrap the calls to such methods in try...catch blocks in the overrides of the parent methods.
Comment #5
mondrakeon this
Comment #6
mondrakeComment #7
mondrakeSAme as #4,
Comment #8
alexpottI wonder if there is a way to indicate to PHPUnit that ExpectationException is a failure and not an error. It feels like something that anything using Mink with PHPUnit would want to be able to do. I'm wondering if we're fixing this the right way around.
Comment #9
mondrakeThanks. I'm not aware of a mechanism to 'register' exceptions in PHPUnit so that they are interpreted as AssertionFailureErrors. Maybe that could be done with listeners in PHPUnit 9, but listeners are a kinda blind alley right now, they're deprecated and will no longer exist in PHPUnit 10, where they're replaced with the event system. So we need to wait and see I guess.
Comment #10
longwaveInstead of changing WebAssert can we just wrap the exception in BTB? Entirely untested but something like:
Comment #11
longwaveThis does seem to work, but breaks the PHPUnit stack trace -
onNotSuccessfulTest()fixes that but then reverts to an error :/Comment #12
mondrakeNice try! The risk I see here is that overriding PHPUnit's methods we get exposed to future changes in PHPUnit itself.
For instance, in PHPUnit 10,
PHPUnit\Framework\TestCase::addWarning()was madefinalso we can not override it any longer inDrupal\Tests\Traits\ PhpUnitWarnings. See #3217904-47: [meta] Support PHPUnit 10 in Drupal 11.Comment #13
longwaveSo this is probably doable, but involves a lot of fighting against PHPUnit and Symfony.
onNotSuccessfulTest() alone can be used to catch and rethrow an ExpectationException as an AssertionFailedError, but this loses the stack trace, even when setting the previous exception - this is because the previous exception is lost when PHPUnit serializes the exception via the process isolation mechanism.
The trace is stored in the constructor of AssertionFailedError, so I tried extending that and throwing a subclassed exception, but then the Symfony debug loader gets in the way and tells me that AssertionFailedError is internal and shouldn't be extended. Also PHPUnit seems to check the exception type directly and doesn't handle my subclass properly somewhere, so the trace is no longer printed at all!
Comment #14
longwaveReflection to the rescue, this is a horrible hack but seems to work. If I break NodeTitleTest:
Without the patch:
With the patch:
It would be cleaner to subclass AssertionFailedError but I don't see how to do that without the debug class loader throwing a deprecation because the class is internal, which gets turned into an error at this point.
Comment #16
mondrakeActually this looks very very nice to me, kudos @longwave!
I think we do not need this with PHP 8.1.
Still a voice in the back of my head tells me to be aware this may change with PHPUnit 10, but let's see that when time comes.
Comment #17
mondrakeIS needs an update for the new direction.
Comment #18
mondrake... and then I think it would be worth un-doing here the try-catch blocks in Drupal's WebAssert extension that are therefore no longer needed.
Comment #19
longwaveUpdated IS. Forgot that setAccessible() is redundant, NW for that and #18.
Comment #20
longwaveComment #21
alexpottPersonally I feel this is worth an upstream issue to ask if there is a way to mark specific exceptions as fails and not errors. It would make integrating PHPUnit with things like mink easier.
Potentially we could use our \Drupal\TestTools\PhpUnitCompatibility\ClassWriter to copy \Behat\Mink\Exception\ExpectationException and make it extend \PHPUnit\Framework\AssertionFailedError instead of exception. But maybe this would fall foul in the same way as reported in #13.
Comment #22
longwaveOpened https://github.com/sebastianbergmann/phpunit/issues/4983
When researching this I stumbled into the process isolation feature request at https://github.com/sebastianbergmann/phpunit/issues/1351 which it turns out originates here :)
Comment #23
mondrakeThanks a lot for opening the upstream issue @longwave.
Comment #25
mondrakeOn this.
Comment #27
mondrakeFixed #16 and #18. Moved the
onNotSuccessfulTest()implementation to a trait, see #3285270-3: Allow core test to only skip tests when a fragile part failed why.Comment #28
mondrakeComment #29
mondrakeComment #30
lendude+1 for moving this to a trait, which would also allow something like Drupal Test Traits to use this.
Comment #31
mondrakeThinking again, the possible issue with this approach is that we will not increase the PHPunit assertions count - maybe we could, but only on failure, if we use
Assert::fail()instead of just throwing the exception, but again we would lose the stack trace IIIC. And we may end up with successful tests reported as 'risky' if only WebAssert assertions are part of a test.Comment #32
longwaveRe #31 not sure I follow, we already have this in BTB::setUp():
Comment #33
mondrakeAh, cool then I think #2805849: WebAssert does not track assertions, Test marked as risky no longer stands.
addToAssertionCounthas been marked@internaland will befinalin PHPUnit 10, but still as long as we do not want to override it should be ok. Thanks and sorry for the noise.Comment #34
longwaveNot sure we are going to get any help from upstream here, so to me this looks good now, except some bikeshedding around the trait name :)
Comment #35
mondrakeThose names would be Mink specific though, and we will have another headache with #3285270: Allow core test to only skip tests when a fragile part failed which is up for something different still out of that same callback.
More general name? or a trait that somehow calls other traits?
Comment #36
longwaveShould the trait implement the method directly, or should it just provide something like
rethrowMinkExceptions()(which would not throw anything in the default case), then it is up to BTB or other implementers to add the trait and implement onNotSuccessfulTest() themselves?Maybe PHPUnit 10's event system will make this cleaner? :)
Comment #37
mondrakeHow about something like this?
Comment #38
longwaveNot sure this is the right way, the trait doesn't really do anything?
Comment #39
mondrakeIt's there as a structural element, to avoid we do not proliferate alternative implementations of the callback.
In #3285270: Allow core test to only skip tests when a fragile part failed we can make it 'do something' as the skipping would be applicable to all test suites. Then non-BTB test suites can implement an empty
unsuccessfulTestHandler()in the base class.Comment #40
longwaveOh I see. I think that makes sense but I do wonder if this should be left for that issue, but I guess it can be added here to save refactoring there.
Let's see what core committers think.
Comment #41
alexpottAdded a comment to the MR - not sure why we have the complicated trait architecture here. I also think we should push harder upstream. https://github.com/sebastianbergmann/phpunit/issues/4983#issuecomment-11...
Comment #42
mondrakePlease see #39. If rationale there is not enough, happy to revisit.
Comment #43
mondrakeRe #41,
Upstream is a no-no. https://github.com/sebastianbergmann/phpunit/issues/4983#issuecomment-12...
Comment #44
alexpottSo given the comment from the PHPUnit maintainer we can go ahead here but also for me the comment from the maintainer is problematic for the current approach. We're reaching in to the internals of PHPUnit and setting \PHPUnit\Framework\Exception::$serializableTrace - this fixes what PHPUnit reports but if there is any later handling that relies on \PHPUnit\Framework\Exception::getSerializableTrace and \PHPUnit\Framework\Exception::getTrace actually matching - well they will not. I remain unconvinced that the benefits here outweigh the possible "what's going on - how come the trace and the serialized trace??!?!" problem. I think we just have to live the PHPUnit behaviour.
Comment #45
alexpottAnd FWIW I think the parts where we are already converting to a PHPUnit exception are incorrect for the reason in #44.
Comment #46
mondrakeI was also thinking - instead of banging our heads against PHPUnit, shall we try Mink instead? I.e. make it possible for
WebAssertto specify a callback to manage the assertion, and implement it with PHPUnit'sassertTrue(). And only if no callback specified, fall back to the private behaviour?
Comment #47
alexpott@mondrake no harm in giving that a go.
Comment #48
mondrakeFiled https://github.com/minkphp/Mink/issues/835 upstream, when I find time I will try a PR.
Comment #49
catchSounds like we should hold off here.
Comment #50
mondrakeSo, also Mink is pushing back hinting at a solution that is what the MR actually implements here, more or less, https://github.com/minkphp/Mink/issues/835#issuecomment-1260726225
Back to square one...
Comment #51
mondrakeI think in the end the solution here is the correct one, but the fact that uses reflection to push the Mink exception trace into PHPUnit internals which is likely not sustainable.
The problem is a PHPUnit's one though, not Drupal's.
So I looked into trying to fix the exception chaining trace issue in PHPUnit AssertionFailedError. With the patch attached here applied to PHPUnit, the MR here applied to Drupal, and NodeTitleTest broken like in #14, now the PHPUnit output will show
Comment #52
mondrakeAny feedback before I start the quest to get the change into PHPUnit?
Comment #53
mondrakeFiled the https://github.com/sebastianbergmann/phpunit/issues/5068 issue upstream, will follow with a PR based on #51.
Comment #54
mondrakePR upstream https://github.com/sebastianbergmann/phpunit/pull/5069
Comment #55
mondrakePHPUnit upstream rejected to implement #5069 and will not work on #5068, so we're on our own again. I will try something else soon.
Comment #56
mondrakeThis is passing the parent stack trace as part of the AssertionFailedError message, and avoid exception chaining that is broken in PHPUnit for tests running in isolation.
Comment #57
mondrakeComment #58
mondrakeFor reference, with the patch here and a broken NoteTitleTest like in #14, we get
Working on @alexpott's feedback.
Comment #59
mondrakeMaybe this is just simpler?
Comment #60
mondrakefixed
Comment #61
mondrake@alexpott we probably do not need a trait then, is that OK?
Comment #62
mondrakeSo, here changed to a trait that only manages the conversion of the Mink exception to a PHPUnit failure - that can be reused elsewhere, and left the onNotSuccessfulTest() implementation in BrowserTestBase.
Comment #63
jonathanshawAn alternative to the trait approach might be to create a new error class for the purpose, that's what I did as a solution for this Mink problem in a similar context: https://github.com/jonathanjfshaw/phpunitbehat/pull/7/files#diff-194e3b5...
Functionally a very similar result, whether it's more or less elegant I'm not sure.
Comment #64
alexpott@mondrake what do you think about #63? I like it.
Comment #65
mondrakeI think @longwave mentioned trying that #13 and #14, we'd have to extend AssertionFailedError here which would throw an error via SymfonyDebugClassloader since that's @internal.
But yes, maybe we no longer need a trait at this point of the MR, we could just have an abstract class with a static method to convert the Mink exception to a failure.
Thoughts?
Comment #66
smustgrave commentedThis issue is being reviewed by the kind folks in Slack, #need-reveiw-queue. We are working to keep the size of Needs Review queue [2700+ issues] to around 400 (1 month or less), following Review a patch or merge require as a guide.
Wonder if the MR could be updated to 10.1.x please moving to NW just for that.
The changes that are there look good to me but see in #65 there may be some more discussion to be had?
Comment #67
mondrakeYes, that's why this issue was in 'needs review'. Changing to 'needs work' does not help here since we do not know what work needs to be done.
Comment #68
smustgrave commentedIf you see the comment though I moved to NW for the MR to be updated as this has missed the 10.0.x mark.
Comment #69
mondrakeYes, I will be happy to do that when I get feedback on #65. I have concerns that bumping the branch for its own sake is not very productive.
Comment #70
smustgrave commentedGotcha! Sounds good thanks.
Comment #71
mondrakeBumped to 10.1.x and refactored to remove the need of the trait. We can't do #63 unless we break a PHPUnit @internal exception which is not good for future compatibility.
Comment #72
jonathanshawI've been an advocate of the trait or abstract class approach so that Drupal Test traits could reuse logic. But looking at the final shape if the MR, I think that consideration is very minor and should not delay this good improvement here.
Comment #73
mondrakerebased
Comment #74
mondrakeFTR, in PHPUnit 10.1 this might be addressed via https://github.com/sebastianbergmann/phpunit/issues/5201
Comment #75
alexpott@mondrake after all the previous discussion https://github.com/sebastianbergmann/phpunit/issues/5201 is quite amusing.
Comment #76
alexpottI think given this won't work on PHPUnit 10 we consider waiting until we update to PHP 10.1 and can use the new capabilities.
Comment #77
mondrakeagree :)
agree... this is an improvement but not strictly necessary. Can wait.
Comment #78
mondrakereparenting to PHPUnit 10 meta
Comment #79
mondrakeComment #80
mondrakeComment #81
mondrakeClosing as duplicate, will be part of #3417066: Upgrade PHPUnit to 10, drop Symfony PHPUnit-bridge dependency. See https://git.drupalcode.org/project/drupal/-/merge_requests/6326/diffs?co...