[META] [policy] Remove PHPUnit assertion messages when possible, and standardize remaining messages

Created on 29 April 2020, almost 5 years ago
Updated 16 January 2023, about 2 years ago

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

When to use a custom assertion message

  1. 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.
  2. 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 like assertCacheKey(), since a message 'Failed to assert that an array has a key' would be too vague.)
  3. 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...

  1. Most assertion messages in core are already written this way (as a hold-over from the SimpleTest days).
  2. Symfony does it this way. See https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Con... for example.
  3. 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.");
    
  4. ...
  1. 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.
  2. 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."
  3. ...

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."

  1. Even more closely matches PHPUnit's default messages.
  2. No chance of ambiguity or misunderstanding.
  3. Self-documenting.
  4. ...
  1. More work: every custom assertion message will have to be changed in all of core - nothing does it this way (yet).
  2. More repetition: every custom message will start with the same 23 chars.
  3. 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".
  4. ...

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.");
      }
    }
  1. No repetition.
  2. The failure messages all make sense.
  3. The test code is self-documenting and makes sense.

Remaining tasks

  1. Continue to flesh out reasons for/against each approach
  2. Decide which approach we're going to standardize on
  3. Figure out where to document it
  4. Decide proper scope for child issues
  5. Open all the child issues
  6. Fix all the child issues to make core consistent about this
  7. 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.

🌱 Plan
Status

Active

Version

10.1

Component
PHPUnit 

Last updated 1 day ago

Created by

🇺🇸United States dww

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • 🇳🇿New Zealand quietone

    Moving to Coding Standards project for discussion.

  • 🇳🇿New Zealand quietone

    On second thought, this is best in core. Sorry for the noise.

  • 🇺🇸United States neclimdul Houston, TX

    I 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.

  • 🇺🇸United States neclimdul Houston, TX

    Didn'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.

  • 🇳🇿New Zealand quietone

    Where do we want to document this?

  • 🇳🇿New Zealand quietone

    It 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?

    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

    1. When the assertion in inside a loop.
    2. In a custom assertion method in a trait or base test class. A custom assertion method begins with public function assert*();.
    3. 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'
Production build 0.71.5 2024