Problem

  • Custom assertion messages are hiding too many details that are important for debugging, in case a test fails.

Goal

  • Introduce smart comparison methods, which allow to expose a custom label, but don't hide debugging details.

Proposed solution

  1. Turn the $message argument in all assertion methods into a base string that is passed into format_string().
  2. This allows custom assertions methods to still output the arguments like the default assertion message would, by using placeholders of @first and @second, or @value.
      $this->assertEqual($original_count, count($users), 'Original user count @first is still @second after saving.');
    
      $this->assertTrue($status, 'Comment status @value found.');
    
  3. Ensure that all default assertion methods either use @first and @second, or @value.

No longer Proposed solution

Reason: PHPUnit implements assertSame() methods, which essentially are our assertIdentical() methods. By introducing a custom assertSame() now, we'd make the future migration to PHPUnit considerably harder.

  1. Add assertSame() and assertNotSame() to TestBase.
  2. Supply a name/label and the two values to be compared:
      $this->assertSame('Username', $user1->name, $user2->name);
    
  3. Or even supply a completely custom assertion message, but leverage the existing placeholders:
      $this->assertSame('Username', $user1->name, $user2->name, '@name: @first == @second.');
      $this->assertSame('Username', $user1->name, $user2->name, '@name is identical.');
    

Notes

Comments

sun’s picture

StatusFileSize
new7.55 KB

Let's move this one step further and also adjust the other base assertion functions accordingly.

This means you can pass a message string of 'Yada yada, @first mea culpa @second, tadaa!' to them and will be logged as you'd expect.

Status: Needs review » Needs work

The last submitted patch, drupal8.assertsame.1.patch, failed testing.

xjm’s picture

sun’s picture

StatusFileSize
new7.83 KB

Fixed the test failures. This pretty much clarifies why #1601202: Make simpletest assertions output both the default and custom message texts won't fly at all.

sun’s picture

StatusFileSize
new8.33 KB

Improved phpDoc and function signature.

sun’s picture

StatusFileSize
new12.09 KB

Example test conversion.

Status: Needs review » Needs work

The last submitted patch, drupal8.assertsame.6.patch, failed testing.

sun’s picture

Status: Needs work » Needs review
StatusFileSize
new12.17 KB

Re-rolled against HEAD.

Can we move forward with this patch, or are there any serious objections?

robloach’s picture

Status: Needs review » Reviewed & tested by the community

PHPUnit has the same assertSame() and assertNotSame(), so this test makes complete sense. I also like that we're already making use of it in some cases.

Looking at PHPUnit's assertNotSame(), I see why it's not "!==". RTBC.

sun’s picture

However, for full disclosure:

The assertSame() and assertNotSame() methods are not really compatible with PHPUnit's. They happen to share the strict type comparison.

But otherwise, especially the first $label argument is custom. The intention is to provide methods that retain the verbose output of "var_export(@first) is identical to var_export(@second)" of assertIdentical(), but additionally allow to prefix that assertion message with a label, so test results can be scanned and debugged more easily.

If that is insufficient for certain assertions, then tests are able to use the assert*() methods being improved by this patch - which now allow to customize the assertion message, while keeping the @first/@second/etc placeholders.

In the end, these improvements will allow us to remove almost all calls to t() and format_string() in test assertions.

catch’s picture

If we switch to PHPUnit at some point, what will happen to these?

catch’s picture

Status: Reviewed & tested by the community » Needs review
sun’s picture

Title: Introduce assertSame() and assertNotSame() » Replace all t() in *TestBase classes with formatString(), and allow custom assertion messages using predefined placeholders
StatusFileSize
new34.21 KB

Alright, let's forget about assertSame().

Status: Needs review » Needs work

The last submitted patch, drupal8.test-formatstring.13.patch, failed testing.

jhodgdon’s picture

I just marked #1803674: Remove t() from default test assertions in TestBase class as a duplicate of this issue. It was taking a simpler approach...

damien tournoud’s picture

+      if (strpos($message, $token) === FALSE) {
+        unset($args[$token]);
+        continue;
+      }

Why is this required?

sun’s picture

Status: Needs work » Needs review
StatusFileSize
new35.26 KB

Added docs for the filtering, and fixed test failures.

sun’s picture

Issue summary: View changes

Updated issue summary.

sun’s picture

Issue summary: View changes

Updated issue summary.

sun’s picture

I've updated the issue summary for the revised/current proposal.

I think this is ready to fly.

jhodgdon’s picture

I looked at the code here... a couple of comments:

a) Why is this @todo here?

+      // @todo Remove all manual var_export() calls from tests and remove this.
+      if (!is_string($value)) {
+        $args[$token] = var_export($value, TRUE);
+      }

It seems like it would actually be fairly common for one of the tokens to be an ordinary string variable value, so I think you would always need this? Also, some of the previously existing code used var_export on string data, such as this bit from assertNoFieldByXPath:

-        $message = t('Found field with name @name', array(
-          '@name' => var_export($name, TRUE),

b) I think it would be useful to document for each $message what placeholders someone can use if they override the default message. For instance on assertTrue/False, document that @value will be replaced by the value in any message, and on assertEqual, @first and @second will be replaced... but maybe that's a separate issue since many of these placeholders already existed.

c) in tearDown()

$message = format_plural($emailCount, '1 e-mail was sent during this test.', '@count e-mails were sent during this test.');

This message is still being translated. format_plural() does translation.

d) in drupalCreateRole()

-          $this->pass(t('Created permissions: @perms', array('@perms' => implode(', ', $permissions))), t('Role'));
+          $this->pass($this->formatString('Created permissions: @perms', array('@perms' => implode(', ', $permissions))), 'Role');

Does it still make sense to implode, since var_export() is being used in formatString()? [this applies to other parts of the patch too]

sun’s picture

Meanwhile, I advanced on this, since I continuously need to re-implement these better assertion messages for individual tests.

Showcasing assertEqual(), which should actually be part of the proposed new formatString():

  /**
   * Overrides TestBase::assertEqual().
   *
   * - actual/expected vs. first/second.
   * - Default message clarifying what's actual vs. expected.
   * - Default message placeholders.
   * - var_export()
   * - Conditional wrapping of actual/expected in PRE elements, if
   *   var_export contains newlines (non-scalar values).
   */
  protected function assertEqual($actual, $expected, $message = NULL, $group = 'Other') {
    $pass = $actual == $expected;
    $actual = var_export($actual, TRUE);
    $expected = var_export($expected, TRUE);
    if (!isset($message)) {
      $message = '@actual is equal to expected @expected.';
    }
    if (strpos($actual, "\n") !== FALSE) {
      $message = str_replace('@actual', '<pre>@actual</pre>', $message);
    }
    if (strpos($expected, "\n") !== FALSE) {
      $message = str_replace('@expected', '<pre>@expected</pre>', $message);
    }
    return $this->assert($pass, format_string($message, array(
      '@actual' => $actual,
      '@expected' => $expected,
    )), $group);
  }
jwilson3’s picture

re @20, I might argue that if either the actual OR the expected had a newline, then *both* should be wrapped in PRE, just for consistency of the visual comparison, no?

jhodgdon’s picture

I am not certain that most/all users of assertEqual put the "actual" value first and the "expected" value second, either? It doesn't seem right to me. assertEqual previously had the semantics of "see if these two values are equal", not "see if this value is what you expect it to be". I don't know that it's always used in the "actual vs. expected" manner. I think it would be better to leave the message as less specific as it used to be.

tstoeckler’s picture

Re #22: Can you explain in what use-case you would use that function that isn't the actual/expected use-case? Both in my own usage and also when reading other people's code that has been the exclusive one I've seen.

jhodgdon’s picture

Well, at a minimum I am not sure that "actual" and "expected" are always in the order that is suggested by this modification.

tstoeckler’s picture

Ahh, okay, I can see that. But don't you think that could be a matter of getting used to the new function? IDE people will even get the autocomplete to tell them what to put where. And to me it feels natural to do $actual first and then $expected, because that is the way we do if{}s in core. I.e. we avoid yoda conditions such as if ('Adam' == $user->name()) but instead we do if ($user->name() == 'Adam').

Sorry for being a little bit persistent, but I see great value in a function as depicted in #20. I wrote a rather comprehensive suite of tests for Libraries API 2.x which consisted of comparing very long arrays, where the $expected value was (minimally) computed, and even after manually inserting the < pre > tags, as that function also does, it was very hard to remember which browser tab was which, and, by extension whether my test was wrong or whether it was actually showing a legit fail. Another example is you see a failed assertion on "NULL is equal to FALSE." which tells absolutely nothing about what is going on.

jhodgdon’s picture

How many core and contrib tests are doing it the other way and would therefore need to be changed?

eric_a’s picture

Note that PHPUnit signatures have $expected as the first argument.

http://www.phpunit.de/manual/3.8/en/writing-tests-for-phpunit.html#writi...

jhodgdon’s picture

Status: Needs review » Needs work

Huge swaths of the previous patch do not apply any more. And I still think it needs work due to the comments above.

pfrenssen’s picture

Issue tags: +Needs reroll
pfrenssen’s picture

Issue summary: View changes

Updated issue summary.

mile23’s picture

Issue summary: View changes
Status: Needs work » Needs review
StatusFileSize
new9.29 KB

I've been writing unit tests for Drupal\simpletest\TestBase. You know, like you do. And t() and other globals make it a pain to test.

I might be hijacking this issue with something a bit out of scope, but regardless of whether its t() or format_string(), this class needs to be unit tested completely, and therefore it can't be a global.

I suggest following the lead of this: https://drupal.org/node/2079611

Basically have a TranslationInterface object as a property, allowing TestBase to have its own t() method. Then replace t() with $this->t().

Doing it correctly allows for dependency injection in unit tests:

  protected function t($string, array $args = array(), array $options = array()) {
    if (empty($this->stringTranslation)) {
      $this->stringTranslation = \Drupal::translation();
    }
    return $this->stringTranslation->translate($string, $args, $options);
  }

The main problem with this approach is that some methods in TestBase are inexplicably static (insertAssert() mainly), and don't know what $this means so t() remains global within them.

Static baaaad. :-)

jhodgdon’s picture

#30 is a completely separate issue. This one is really only about test assertion messages, not about other translations. Please open a new issue. Thanks!

mile23’s picture

Right. Test assertion messages are currently run through t(). The issue is using str_replace() and format_string() instead, in order to allow predefined placeholders.

I point out that t() and format_string() make it difficult to write unit tests, so please do dependency injection the right way.

Which is not off-topic.

jhodgdon’s picture

Title: Replace all t() in *TestBase classes with formatString(), and allow custom assertion messages using predefined placeholders » Allow custom assertion messages using predefined placeholders

The title of this issue was misleading, so I have updated it to reflect the current issue summary. I think we have fixed all the test assertions' use of t(), on a separate meta-issue. If there are still t() calls that shouldn't be there, that is a separate issue and should be referencing the other meta-issue.

This issue is specifically about making the base tests assertions have a "base string" argument that would be passed through format_string() so that standard substitutions could be made.

mile23’s picture

olli’s picture

sweetchuck’s picture

I would like to reroll the patch #30, but not exactly clear to me what is the expected result. (Assertion message).
Do we need replace the String::format() with $this->t() ?
And what about the format_plural()?

jhodgdon’s picture

RE #36, the patch in #30 is out of scope, see #31 ... I really don't know what it was trying to do, but we absolutely do **not** want to be translating test assert messages. We spent a huge amount of effort on several other issues to make sure we were not translating them, and as far as I can tell, the patch in #30 is trying to translate them again. I don't know why.

We probably need to go back to the patch in #17, get it rerolled, and address the comments in #19-#25, rather than starting with the patch in #30.

sun’s picture

Status: Needs review » Needs work

Yeah, #30 is strange. All calls to t() have been replaced with String::format() in the meantime.

Just to recap on the original idea here:

Currently, you only have two options:

  1. Use the default assertion message: $this->assertEqual('foo', 'bar');
  2. Use a custom assertion message: $this->assertEqual('foo', 'bar', 'foo is somewhat equal to bar.');

The fundamental idea here is to introduce a third option:

  1. Use a custom assertion message with the default placeholder values: $this->assertEqual('foo', 'bar', '@first is somewhat equal to @second.');

The values for @first and @second are the first and second arguments respectively, and automatically var_export()'ed.

For assertion methods that only accept a single argument, the placeholder is just simply @value.

This allows you to easily use more meaningful custom assertion messages, while still retaining the output of the expected vs. actual variables in the assertion message (like the default assertion message would do).

Currently, our tests are littered with custom assertion messages that are completely hiding the expected + actual values from the assertion message. Due to that, developers often have to temporarily hack failing tests to remove the custom assertion messages, so as to see what's actually going wrong.

To accomplish this, the patch introduces a TestBase::formatString() method, which essentially String::format(), but additionally var_export()s the placeholder values.

sweetchuck’s picture

Status: Needs work » Needs review
Issue tags: -Needs reroll
StatusFileSize
new31.26 KB

I think the isset() returns TRUE for empty strings.

$message = '';
if (!isset($message)) {
  // Never step into here.
}

The default value of the $message argument in most of the assert*() method is an empty string, which is checked with if (!isset($message)) {, therefore I changed the check to if (!$message) {.

I think it would be better to change the default value of the $message argument to NULL from empty string.

Status: Needs review » Needs work

The last submitted patch, 39: drupal-custom-assertion-messages-1601146-39-d8.patch, failed testing.

sweetchuck’s picture

Status: Needs work » Needs review
StatusFileSize
new31.26 KB
jhodgdon’s picture

Status: Needs review » Needs work

Yeah, a patch that passes the tests! Going forward, please include an interdiff with each new patch, so we can see what changed. For now, we can use the patch in #41 as a baseline.

So, I think this looks pretty good. I have a few suggestions for the documentation:

a) In the formatString() method docs, it refers to format_string() but it actually passes the formatting to String::format().

b) assertTrue() docs:

+  /**
+   * Check to see if a value is not false (not an empty string, 0, NULL, or FALSE).

- Check ==> Checks

c) All assert methods using the new formatString() calls: We need to document what the default message and placeholders are, in the @param $message for each method, so that users of that assertion get this information. And we can probably take out the part that is in all of them about using String::format().

As an example, I would suggest this for assertTrue:

@param $message

(optional) A message to display with the assertion, which defaults to 'Value @value is TRUE.' You can use @value as a placeholder in your message for the $value parameter (it will be passed through var_export() and appropriately formatted). Do not translate messages.

d) I am not sure why some of the messages are using ! and some are using @ for formatting. I think we should pick one and use it consistently.

Hmm... Given the code in the TestBase::formatString() method, which sometimes inserts PRE tags, I think @ is not a good idea. Wouldn't @-formatting take those PRE tags and do check_plain() on them, which would make them not be tags any more? So probably we should be using ! instead of @ in the default messages that are inserting the input variables? Maybe we should test this and see what happens if you do:
assertTrue(array('foo' => 721));
or something like that.

e) Some of the changes in this patch aren't all that great:

-      $message = String::format('Current URL is @url.', array(
-        '@url' => var_export($this->container->get('url_generator')->generateFromPath($path, $options), TRUE),
+      $message = $this->formatString('Current URL is @url.', array(
+        '@url' => url($path, $options),

We still need to do $this->container->get('url_generator')->generateFromPath() not url() here.

no_angel’s picture

.

jhodgdon’s picture

Version: 8.0.x-dev » 8.1.x-dev
Assigned: sun » Unassigned

We should still consider doing this, but it would be 8.1 now.

Version: 8.1.x-dev » 8.2.x-dev

Drupal 8.1.0-beta1 was released on March 2, 2016, which means new developments and disruptive changes should now be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

wim leers’s picture

This would be so valuable, let's do this!

wim leers’s picture

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.0-beta1 was released on August 3, 2016, which means new developments and disruptive changes should now be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.0-alpha1 will be released the week of January 30, 2017, which means new developments and disruptive changes should now be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.0-alpha1 will be released the week of January 17, 2018, which means new developments and disruptive changes should now be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

berdir’s picture

Status: Needs work » Closed (outdated)

I believe this is no longer relevant, phpunit assertions displays both the message and the values, maybe some of our legacy/BC assertions could be improved but should mostly be fine.