Problem/Motivation
Core uses PHPUnit for our automated tests. All PHPUnit assertion methods allow for an optional custom message. In PHPUnit, these messages are only displayed when a given assertion fails. If no message is defined, the assertions provide a default message themselves.
In many cases, the best solution is to remove the custom message and let the default message from PHPUnit appear when the assertion fails. Removing duplicate or extraneous messages is worthwhile whenever possible. However, there are definitely still cases where a custom message is helpful or even necessary. For example, an assertion inside a loop needs a custom message so you know which loop iteration you were on when the assertion failed.
PHPUnit itself is un-opinionated on whether these messages should be written in a "positive" (aka "confirmative") way (what we're expecting to be true, which was violated if the assertion fails) or a "negative" one (what went wrong that caused the assertion to fail). All PHPUnit itself gives us is this:
https://github.com/sebastianbergmann/phpunit/blob/master/src/Framework/C...
* @param string $description Additional information about the test
Back when we used SimpleTest, all the assertions came with a manually crafted assertion message. These messages were displayed in the UI to show what passed or failed. The message was green if the assertion passed, and red if it failed. So it made sense for us to always write the messages in the "positive" form. Most test assertions in core, when ported to PHPUnit, kept the messages phrased for the positive case.
It doesn't really matter if we interpret the additional information about the assertion failure as negative or positive. But what we have now is a really confusing mix of both. Sometimes, the message explains what failed (negative). Most of the time, it explains what we were expecting (positive).
Proposed resolution
Update contents of https://www.drupal.org/docs/develop/automated-testing/phpunit-in-drupal/... with the following
Assertion messages
The default assertion message provided by PHPUnit is sufficient for most situations. The page explains when a custom assertion message can be added and the format of the message.
When to add a custom assertion message
- When the assertion in inside a loop.
- In a custom assertion method in a trait or base test class. A custom assertion method begins with
public function assert*();.- In a helper method.
Format of a custom assertion message
%subject% should %verb% %payload%Definitions
%subject%
The additional context information
%verb%
The action.
%payload%
The value the%subject% should be.
Example
foreach ($expected_steps as $element_id => $step_value) { foreach (['s', 'i', 'h', 'd', 'm', 'y'] as $sub_field) { $name = $element_id . '[' . $sub_field . ']'; $expected_step = $step_value[$sub_field] ?? 1; $input = $this->xpath("//form//input[@name='$name']"); $this->assertCount(1, $input, "Duration input $name should appear exactly once."); $actual_step = (integer) $input[0]->attributes()->{'step'}; $this->assertEquals($expected_step, $actual_step, "Duration input $name should have the correct step value."); } }where
{subject} == "Duration input $name" should == 'should' ;) {verb} == 'have' {payload} == 'the correct step value'
--- original proposal ---
When to use a custom assertion message
- In a normal test method (i.e. those starting with
public function test*();), do not add messages UNLESS you are in a loop AND you want a failure to report context information that is not present in the assert arguments. - In custom assertion methods (i.e. those starting with
public function assert*();) contained in traits or base test classes, have a message saying what's expected. This is because that test code is executing there as a result of an assert* call from a normal test method, and it's useful to report 'where' you are when the test is failing. (purely for example - it's more useful to report that you are missing a cache key if you have called an assertion likeassertCacheKey(), since a message 'Failed to assert that an array has a key' would be too vague.) - Similarly if you are asserting within a helper method. The helper method would be called from within test methods with some arguments, and the content of those arguments should be used in messages to provide context.
If needed, what kind of custom message should we use?
We need to pick one of these, document it, and then open child issues to make sure all of core does it consistently. Assuming that the pro for one approach is a con for the other, I'm just collecting reasons to pick a choice in the list under each sub heading...
Positive messages (what we're expecting if it passes)
- Most assertion messages in core are already written this way (as a hold-over from the SimpleTest days).
- Symfony does it this way. See https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Con... for example.
- It arguably makes the test code easier to read/understand, since the message helps document what the assertion is testing for. It's generally easier for our minds to make sense of things phrased positively, instead of having to invert meanings and understand things as negations. For example:
$this->assertFieldByXPath("//form//input[@name='$name' and @step='$step']", NULL, "Found form input $name with step $step.");or
$input = $this->xpath("//form//input[@name='$name']"); $this->assertCount(1, $input, "Found form input $name."); - ...
Negative messages (what went wrong if it failed)
- The message is only printed if something failed, so it makes sense that the message would be saying what went wrong, not what we were hoping was true.
- Default PHPUnit assertion messages (when we don't define our own) are generally phrased this way. For example:
"Failed asserting that 0 matches expected 1." - ...
Be Explicit: Declare the meaning as part of the message
Instead of picking positive or negative and relying on convention or documentation for it to make sense, we could adopt PHPUnit's basic approach, and start all assertion messages with "Failed asserting that..." so readers immediately understand the message, instead of having to know how to interpret it. E.g. instead of: "Field X exists" vs. "Field X not found", the message should say "Failed asserting that field X exists."
Pros
- Even more closely matches PHPUnit's default messages.
- No chance of ambiguity or misunderstanding.
- Self-documenting.
- ...
Cons
- More work: every custom assertion message will have to be changed in all of core - nothing does it this way (yet).
- More repetition: every custom message will start with the same 23 chars.
- The default assertion messages are always displayed, even if a custom message is defined. So there's also repetition for each failed assertion where we'd have 2 errors that both start with "Failed asserting that".
- ...
Say what's intended, but phrase it conditionally (so it makes sense when it fails)
See #21:
Custom assertion messages will have the form:
{subject} should {verb} {payload}
For example:
foreach ($expected_steps as $element_id => $step_value) {
foreach (['s', 'i', 'h', 'd', 'm', 'y'] as $sub_field) {
$input = $this->xpath("//form//input[@name='$name']");
$this->assertCount(1, $input, "Duration input $name should appear exactly once.");
$actual_step = (integer) $input[0]->attributes()->{'step'};
$this->assertEquals($expected_step, $actual_step, "Duration input $name should have the correct step value.");
}
}
Pros
- No repetition.
- The failure messages all make sense.
- The test code is self-documenting and makes sense.
Remaining tasks
- Continue to flesh out reasons for/against each approach
- Decide which approach we're going to standardize on
- Figure out where to document it
- Decide proper scope for child issues
- Open all the child issues
- Fix all the child issues to make core consistent about this
- Rejoice
User interface changes
N/A -- only changes to the output when test assertions fail.
API changes
N/A
Data model changes
N/A
Release notes snippet
TBD. Probably not. Only impacts developers.
Comments
Comment #2
jungle@dww, thank you for filing this!
Quoting opinions form @mondrake and @catch, FYI. And reposting my comment here, to sum up my own opinions/suggestions:
a) Per pair of assertions (For example,
assertIsArray()/assertIsNotArray()).b) Or per group of assertions (For example,
assertIs*(), includingassertIsString(),assertIsNotString(),assertIsArray(),assertIsNotArray()etc.)And I think it's acceptable having larger patches if we go for a larger scope eventually, even though it's not encouraged, As all are tests related, nothing won't break.
Comment #3
dww@jungle: Indeed, as I wrote in the 2nd paragraph of the summary:
Deciding if we need custom messages doesn't really matter what kind of assertion it is, it matters where the assertion lives. If there's no loop, and the line number is enough to know what failed, remove the message and rely on the default. But in any case where there's a loop, or something else that makes the line number insufficient to know exactly what assertion failed, we need a custom message (not an inline comment). In those (and only those) cases, we need to have a clear policy for how the messages are written.
Thanks,
-Derek
Comment #4
jungleAdded the demo purpose code which contains a loop, to core/modules/node/tests/src/Unit/PageCache/DenyNodePreviewTest.php. the following is the error message.
I think combining the error message with the line number, that's enough/sufficient to debug.
I do not understand why the loop matters and needs to be discussed specially. Maybe if there is a loop, the test needs to be refactored to use data provider.
Thanks!
Comment #5
dwwFor a concrete example where #4 is definitely not sufficient, check out the Kernel test here: #3020676-11: Support the #date_increment property in the 'duration' form element.
The test is a form, and the form contains a ton of 'duration' elements, each of which contains 6 actual form inputs. The test does this:
Without those custom messages, if something fails, you'd have no way to know what element or step was broken relying only on the default message from
assertCount()orassertEquals().So, then we're back to the question at hand... should the message say:
A) "Duration input $name exists."
B) "Duration input $name not found."
C) "Failed asserting that duration input $name exists."
?
Comment #6
jungleMake sense, @dww, thanks!
How about defining the variable in the message inside a loop as a context, if there is at least one context in the message inside a loop, the message stays. Otherwise, it seems ok to remove.
Comment #7
catchStandardising, as (or in the process of) removing as many custom assertion messages as possible makes sense.
If we switch to negative, then I think we should adopt the 'Failed asserting that...' syntax from phpunit. phpunit is really doing
Failed to assert that [positive statement]rather than[negative statement inverting the meaning of the assertion]. Just the negative statement might sometimes work when you're reading test results, but when reading the test code it's very confusing.Comment #8
mondrake#7 please note though that on assertion failure, PHPUnit will print out BOTH the custom message AND the standard message, for example
So a custom message starting with 'Failed asserting...' will be a bit of a repetition.
In web assertions, currently we have a message that represents what was found and what was expected, for example
and such message cannot be overridden, since Mink does not manage custom messages.
But I think that could be a good way forward - fact-based - to be adopted directionally:
Found {this} in the context of {whatever}, but {that} was expected.My 2c.
Comment #9
mondrakeElaborating a bit on #8, also to reflect a practical attempt we launched in #3131816: Replace assertions involving calls to is_array() with assertIsArray()/assertIsNotArray(), a suggestion could be:
{something} is expected to {verb} {subject} [, but {actual}].Examples:
Comment #10
dwwThanks for all the input and feedback, everyone!
@jungle Re: #6: I don't quite follow what you mean with this:
Isn't that what the example I posted in #5 is doing? Not sure your point.
@catch Re: #7: 💯agree. That's exactly what the Be Explicit option in proposed resolution is about. That's my favorite, too.
@mondrake Re: #8: I think a little repetition ("Failed asserting that") is fine. It's way preferable to raw negation messages (which make the test code itself extremely confusing), IMHO.
Re: #9: Yeah, I could possibly get behind
{something} is expected to {verb} {subject} [, but {actual}].. But maybe "Failed asserting that [positive]" is sufficient (and easier to port core to be using), especially since the default message (that's going to include actual vs. expected in 99% of the cases) will handle those details for us.Again, I think the primary use-case for custom messages is to handle assertions inside loops where a @dataProvider doesn't make sense. So the main point of any remaining custom messages would be to identify which loop iteration actually failed. If we're going to document the resulting policy, I think we should be explicit about this point.
Thanks again,
-Derek
Comment #11
dwwBased on Slack flack from @moshe, giving this a more accurate title... ;)
Comment #12
dwwComment #13
moshe weitzman commentedComment #14
moshe weitzman commentedComment #15
longwaveDoes the message even have to be positive or negative? Can it just state the useful facts and nothing else?
Comment #16
jungleRe #10, in #6 i was saying a pattern to identify assertion messages in a loop which should stay. the pattern is if there is a variable in assertion message, the assertion should stay. Because i found the difference between #4 and #5 is one has $variable in assertion message, the other does not. To me, such variables are contexts.
Don't be surprised if what I said in English is weird, :p.
Thanks!
Comment #17
kim.pepperFailed to assert that [positive statement]Has a big +1 from me.
Comment #18
chi commentedOne more reason to remove assertion messages.
https://github.com/sebastianbergmann/phpunit/issues/4198
Comment #19
neclimdulOh this is a nice issue! Some opinions.
I'm a pretty big -1 on adding "Failed to assert that..." boiler plate. I get that is sets the tone for the message but encourages repeating the assertion phpunit is going to provide for coding standard completeness and provides little meaning leaving us back where we are now.
100% on board that we over use messages. Phpunit's assertion messages are _generally_ very good too and I've been seeing recent commits converting bad "equals/same" and "true/false" assertions to more appropriate assertions so kudos to those developers for laying some ground work on this because that's kind of key :)
I think longwave is right, "positive" isn't a great descriptor but the intent is accurate. Providing context is the key, it needs to be affirmative contextual information not a negative statement for all the reasons pointed out.
Example time. The count example is kinda the ideal case for messages improving the context. Especially if you modify it slightly. Changing
to
You then know both what and why we're counting. For example you immediately know 0 means "oh the the field didn't show up?" or 2 means "oh 2 instances? I duplicated the field?" The existing message would have you digging in the test to see why the test cares about "Form input X"
Back to the boiling plate, phpunit will give us an error like:
While the suggested format would be something like:
I guess its open to opinion which is better, the second does provide the extra context but I think it more reads repetitive with the redundant messaging not providing any additional meaning.
Comment #20
dww@neclimdul re: #19: Thanks for your thoughts. For the record, in the summary I'm proposing:
I agree "Failed to assert instances of form input 'title'." is weird.
That's why I like "Failed asserting that field X exists.". Although as you point out, the assertion could also fail if the xpath found 3 instances. ;) /shrug
Regardless, I'm glad we seem to have universal agreement that we should remove the custom message most of the time, and that work is already underway, so yay. Once that's complete, we'll have a much smaller # of custom messages to still deal with, so whatever we decide will be easier to implement.
Thanks everyone. Please keep the suggestions coming. :)
Cheers,
-Derek
Comment #21
dwwMore examples from my Kernel test at #3020676: Support the #date_increment property in the 'duration' form element. that's partly what prompted me to open this issue. ;)
Here's another proposal:
If you break the code for the first case, you see this:
If you break the code for the 2nd case, you see:
A) No repetition.
B) The failure messages all make sense.
C) The test code is self-documenting and makes sense.
I think this is a pretty good compromise on all the various concerns. To generalize the recommended form:
{context} should {verb} {subject}{context} == "Duration input $name"
should == 'should' ;)
{verb} == 'have'
{subject} == 'the correct step value'
Thoughts? Should we call this a 4th option in the summary? It's sort of a cross between the 'positive' and 'explicit' cases.
Thanks,
-Derek
Comment #22
neclimdulI'm leery of strict rules in documentation but maybe this isn't that. Either way I think you perfectly translated my thoughts into a clear format. Thanks and one vote for that! :-D
Comment #23
mondrakeJust c/p here a post from #3131348-34: Replace assertions involving calls to empty() with assertEmpty()/assertNotEmpty()/assertArrayNotHasKey(), purely to foster discussion. Thanks.
I am not in a better place :)
I can only say my rule of thumb at the moment - just based on few threads here and there. That's the one I followed for #3131348-30: Replace assertions involving calls to empty() with assertEmpty()/assertNotEmpty()/assertArrayNotHasKey().
public function test*();), do not add messages UNLESS you are in a loop AND you want a failure to report context information that is not present in the assert arguments.public function assert*();) contained in traits or base test classes, I try to have a message saying what's expected. This is because that test code is executing there as a result of an assert* call from a normal test method, and it's useful to report 'where' you are when the test is failing. (purely for example - it's more useful to report that you are missing a cache key if you have called an assertion likeassertCacheKey(), since a message 'Failed to assert that an array has a key' would be too vague.){context} should {verb} {subject}to craft the messages.Comment #24
longwaveRe #23.2 and maybe #23.3 this was my intent behind #3133288: [meta] Add custom PHPUnit assertions for common tests - for the example given we could convert
assertCacheKey()to a real PHPUnit assertion and then the default message would be useful ("Failed to assert that the cache key...") and the $message argument only needed to provide extra context.Comment #25
mondrake#24 yes, +1. I actually tried a first stab at that in #3133355: Introduce PHPUnit-compliant WebAssert::responseHeaderExists() method, and its negative.
There are hundreds of custom assertions in test code, try
git grep 'function assert'. Trying and 'PHPUnitizing' them will be a major effort...Comment #26
dwwRe: #23 That list for when to have a custom message looks great to me! Copying points 1-3 into the proposed resolution in a new When to use a custom assertion message subsection.
Also, added what seems to be our current favorite solution as a new Say what's intended, but phrase it conditionally option under If needed, what kind of custom message should we use?.
Thanks,
-Derek
Comment #29
quietone commentedCorrect me if I am wrong, but shouldn't this be moved to the Coding Standards project?
Comment #30
neclimdulMaybe. I think the current running consensus on this also doesn't match the title or issue summary.
Comment #34
xjmRe: #29, core can adopt a best practice without it being the standard for all contrib as well, which is what the coding standards project is for.
Comment #36
quietone commentedMoving to Coding Standards project for discussion.
Comment #37
quietone commentedOn second thought, this is best in core. Sorry for the noise.
Comment #38
neclimdulI think this is stalled pretty hard since the discussion is getting close to 5 years. However, I feel like there was pretty strong consensus around settings some guidance at least that messages should be positive/"confirmative"/affirmative additional messages, letting the assertion show the negative/failure information and the message provide the context for what the failure means.
In the interest of closing issues, could we more forward with just that and if there's apatite for removing or suggesting the removal that could be handled separately? I'm biased because I had strong feelings but I feel like that was always the part we where aggressively agreeing over.
Comment #39
neclimdulDidn't mean to change the title. I started to take a stab and didn't have enough caffeine in my system yet to have a good suggestion.
Comment #40
quietone commentedWhere do we want to document this?
Comment #41
quietone commentedIt is still not clear where this should go. It could be added as a new page in the guide. PHPUnit in Drupal. That would encourage the practice now and, if desired, an issue can be opened in Coding Standards to formally accept the practice.
I read through the issue to create a a bare minimum proposed text for the changes, which follows. What do you think?
Comment #42
quietone commentedMove the proposal from previous comment to the issue summary
Comment #43
quietone commentedComment #44
quietone commentedI put the content in https://www.drupal.org/docs/develop/coding-standards/drupal-simpletest-c...
Comment #45
neclimdul"The default assertion message provided by PHPUnit is sufficient for most situations."
Still strongly disagree with this.
Something like "Failed asserting that false is not equal to false." is entirely insufficient and such assertion failures are most situations.
Comment #46
xjmThis issue has come a long way since the last time I looked at it -- nice work everyone!
I think the one thing that I might change is to start off with a stronger statement about not using assertion messages. Possibly framing it as a coding standard, something like "Custom test assertion messages MUST NOT be used, unless one of the following is true": or something along those lines. Along with the why: Using custom assertion messages actually hides valuable debugging information and misleads the developer. We want them to read the test.
Thoughts?
Comment #47
xjmOh, I see it's already in the handbook. I'll make an edit there directly and post back here for review.
Comment #48
xjmAdded some revisions.
That should make it clearer why it is preferred not to use assertion messages unless they are absolutely necessary. I also made it clearer what kind of "assertion message" we are talking about, since the word "assertion" is very overloaded in our ecosystem and might be interpreted to mean other things.
Edit: Although it looks like I'm going in the opposite direction from @neclimdul. @neclimdul, it's our (core committers') experience that folks' custom assertion messages more often hide more than they reveal, and it actually makes debugging harder. The preferred workflow is to open the test and look at the line where the assertion failed to understand what it means when FALSE is TRUE.
We definitely often mark core issues NW asking for the assertion message to be removed, probably for 8 or more years now.
Comment #49
xjmFor reference, this issue came up in the context of work done for #3128573: [meta] Replace assertions with more appropriate ones, which actually required removing many many assertion messages throughout the core codebase in huge cleanup efforts in order to unblock those conversions. Broadening the scope from the removals required by those conversions to a general policy was sort of several different streams of practice converging, since we already had a long history of removing redundant assertion messages as a reason to NW core issues, and frustrations from core devs in debugging when bad assertion messages misled them.
Comment #50
xjm@cmlara raised a concern that, technically, the handbook page is in the coding standards section as if this is already an approved coding standard that applies to all of contrib as well as core, whereas it's only the draft of a policy formalizing core practice for an NR core issue as per #41. Not sure what to do about that.
Comment #51
quietone commentedThe page was originally in the PHPUnit in Drupal guide titled, "Drupal SimpleTest coding standards". At the time, and with no other contributor feedback, it made sense to move it to Coding Standards.
I will move it back in a bit.
Comment #52
quietone commentedI fixed my error by moving the page back, it is at https://www.drupal.org/docs/develop/automated-testing/phpunit-in-drupal/....
Comment #53
alexpottThis issue concerns me. For me, making this a coding standard would be a mistake. There's far too much nuance at play. For example - look at the assertions being added by #3567544: Alias lookup by path is not case sensitive but AliasPrefixList and AliasRepository::preloadPathAlias's return value is leading to inconsistencies
The first assertion above is in HEAD and the second is being added. I agree that the first message is completely pointless. But the second message is important. The default assertEquals message is not going to clue you in that the purpose of this assertion is about case-insensitivity.
Secondly, assertFalse, assertTrue, assertNull messages are useless and it could be argued that the opposite of this issue should be enforced - adding context for why you are making that assertion is useful.
I think this issue had more merit back in the simpletest days when using a custom message meant that you often lost the default message from the assertion that was often more useful especially when testing equivalence. But with PHPunit custom messages are appended to the default message so that is no longer an issue.
Comment #55
quietone commentedSince about 2012 the document included 'coding standard' in the title. But this is not a coding standard, it is a best practice or guidelines about the assertion messages. Today, I changed the title to be descriptive that it only about assertion messages and when to use them.
The document is in the Develop section so can be improved by the community on an ongoing basis. If anyone wants to have a standard for the assertion message that can be enforced, open an issue in the Coding Standard project. Thanks.
I have updated credit and setting to RTBC.
Comment #56
catchYes I think this is fine, we've moved away from custom assertion messages in a lot of places but equally we can't enforce not having them because sometimes it makes things easier if the default one would be too cryptic.