Follow-up to #2560729: Cast objects implementing SafeStringInterface to string in TestBase / WebTestBase

Part of #2557113: Make t() return a TranslationWrapper object to remove reliance on a static, unpredictable safe list

In #2560729: Cast objects implementing SafeStringInterface to string in TestBase / WebTestBase we did not convert TestBase::assertIdentical(), so we will need to fix the tests themselves for comparisons where the comparison may include an object implementing SafeStringInterface.

Usuallly an equality check would be enough for a simple string comparison, or array comparison where the order doesn't matter. If the order of the array matters, we can use TestBase::castSafeStrings() before testing identicalness.

Beta evaluation: part of a major issue

Comments

stefan.r created an issue. See original summary.

stefan.r’s picture

Status: Active » Needs review
StatusFileSize
new12.89 KB

for review-ability it may be useful to post another patch that does explicit failures with the values of the compared variables in them - just so we can check that we don't lose test coverage by switching to assertEqual().

stefan.r’s picture

Title: Refactor assertIdentical() checks on values that may safe string objects » Refactor assertIdentical() checks on values that include safe string objects
stefan.r’s picture

StatusFileSize
new24.9 KB

Adding a few more that were also breaking

Status: Needs review » Needs work

The last submitted patch, 4: 2566447-4.patch, failed testing.

The last submitted patch, 4: 2566447-4.patch, failed testing.

stefan.r’s picture

Status: Needs work » Needs review
StatusFileSize
new36.61 KB
stefan.r’s picture

StatusFileSize
new37.37 KB
stefan.r’s picture

Parent issue is green so seems we have them all now. Not sure the suggestion in #2 is even needed as these clearly didn't need to be identical checks. Maybe someone can do a manual review to confirm we're not losing any test coverage here?

joelpittet’s picture

@stefan.r there may be a chance here that we are making a test less reliable to detect changes. I usually prefer identical if I can get away with it.

It would be nice to see how many of these are still failing and remove the unnecessary changes, what do you think?

  1. +++ b/core/modules/breakpoint/src/Tests/BreakpointDiscoveryTest.php
    @@ -193,6 +193,7 @@ public function testBreakpointGroups() {
    +    $this->castSafeStrings($expected, $breakpoint_groups);
    

    Is this still needed?

  2. +++ b/core/modules/system/src/Tests/Common/FormatDateTest.php
    @@ -87,12 +87,12 @@ function testAdminDefinedFormatDate() {
    -    $this->assertIdentical(format_date($timestamp, 'custom', 'l, d-M-y H:i:s T', 'America/Los_Angeles', 'en'), 'Sunday, 25-Mar-07 17:00:00 PDT', 'Test all parameters.');
    ...
    +    $this->assertEqual(format_date($timestamp, 'custom', 'l, d-M-y H:i:s T', 'America/Los_Angeles', 'en'), 'Sunday, 25-Mar-07 17:00:00 PDT', 'Test all parameters.');
    

    Like all the format_date ones shouldn't need this right?

stefan.r’s picture

@joelpittet I am aware of this potentially making tests less reliable but if we're comparing two arrays of strings where the order doesn't matter, or just two strings we often don't need to be doing an identical check...

I don't think any of these conversions are unnecessary, all of them were caused by specific test fails - neither are any failing anymore.

As to your review:

1. I believe we do need it - this is a case where an equality check would not be appropriate, as the comment says "Ensure the order is as expected. Should be sorted by label." So in those cases we can cast the safe strings and then check identicalness.
2. Maybe not all of them, but date_format can return a t()'ed string under some circumstances, and I remember at least one of those did.

stefan.r’s picture

StatusFileSize
new43.17 KB

To help review-ability I've annotated the conversions. So as we think we have them all right, we can remove them and mark RTBC?

The views ones should arguably be identical checks if we want to check for the order (same for the CKEditor settings), but then we'd have to do an ugly $this->castSafeStrings() everywhere and assign the values to be compared to separate variables...

Status: Needs review » Needs work

The last submitted patch, 12: 2566447-12.patch, failed testing.

wim leers’s picture

+++ b/core/modules/ckeditor/src/Tests/CKEditorAdminTest.php
@@ -81,6 +81,7 @@ function testExistingFormat() {
+    // Begin expected settings array:

@@ -111,7 +112,8 @@ function testExistingFormat() {
+    // ^ End expected settings array.

We never do this, and it's really rather pointless, because that's already what the variable name indicates?

The changes in CKEditor look fine — I manually checked: https://3v4l.org/ei98N. I dislike that we're making tests less strict though, but if it's done for The Better Good overall, then +1.

stefan.r’s picture

@WimLeers the pointless comments were added because the patch is otherwise unreview-able - they would be taken out later :)

But actually this is all way too complicated, @dawehner came up with a much more review-able solution:

1:10 PM <dawehner> stefan_r: so array_map('strval', $array1); ... + === doesn't cut it?
1:11 PM <stefan_r> dawehner: yes but that will cast everything to string... I just want the translationwrappers
1:12 PM <stefan_r> dawehner: we have $this->castSafeStrings in TestBase which does what we want
1:12 PM <stefan_r> but that will make the patch more complicated though
1:12 PM <dawehner> stefan_r: should we introduce assertIdenticalWithTranslationWrappers?
stefan.r’s picture

Title: Refactor assertIdentical() checks on values that include safe string objects » Use assertIdenticalWithSafeStrings() instead of assertIdentical() on values that include safe string objects
stefan.r’s picture

Status: Needs work » Needs review
StatusFileSize
new38.05 KB

Status: Needs review » Needs work

The last submitted patch, 17: 2566447-17.patch, failed testing.

The last submitted patch, 17: 2566447-17.patch, failed testing.

stefan.r’s picture

Status: Needs work » Needs review
StatusFileSize
new38.03 KB
new38.05 KB
dawehner’s picture

Its sad that we need that, but hey, at least it adds some semanticness into our tests, which makes it clearer what is going on!
+1 for introducing a helper assertion.

joelpittet’s picture

+++ b/core/modules/simpletest/src/TestBase.php
--- a/core/modules/system/src/Tests/Common/FormatDateTest.php
+++ b/core/modules/system/src/Tests/Common/FormatDateTest.php

+++ b/core/modules/system/src/Tests/Common/FormatDateTest.php
@@ -88,10 +88,10 @@ function testAdminDefinedFormatDate() {
-    $this->assertIdentical(format_date($timestamp, 'custom', 'l, d-M-y H:i:s T', 'America/Los_Angeles', self::LANGCODE), 'domingo, 25-Mar-07 17:00:00 PDT', 'Test translated format.');
...
+    $this->assertIdenticalWithSafeStrings(format_date($timestamp, 'custom', 'l, d-M-y H:i:s T', 'America/Los_Angeles', self::LANGCODE), 'domingo, 25-Mar-07 17:00:00 PDT', 'Test translated format.');

My test results https://www.drupal.org/pift-ci-job/32267
Show this is not needed.

joelpittet’s picture

StatusFileSize
new8.61 KB
new33.66 KB

This removes some unnecessary changes. Here's the whole patch green #2465399-82: IGNORE: Patch testing issue

lauriii’s picture

  • +++ b/core/modules/views/src/Tests/ModuleTest.php
    @@ -157,40 +157,40 @@ public function testLoadFunctions() {
    -    $this->assertIdentical(array_keys($all_views), array_keys(Views::getAllViews()), 'Views::getAllViews works as expected.');
    +    $this->assertIdenticalWithSafeStrings(array_keys($all_views), array_keys(Views::getAllViews()), 'Views::getAllViews works as expected.');
    ...
    -    $this->assertIdentical(array_keys($expected_enabled), array_keys(Views::getEnabledViews()), 'Expected enabled views returned.');
    +    $this->assertIdenticalWithSafeStrings(array_keys($expected_enabled), array_keys(Views::getEnabledViews()), 'Expected enabled views returned.');
    ...
    -    $this->assertIdentical(array_keys($expected_disabled), array_keys(Views::getDisabledViews()), 'Expected disabled views returned.');
    +    $this->assertIdenticalWithSafeStrings(array_keys($expected_disabled), array_keys(Views::getDisabledViews()), 'Expected disabled views returned.');
    ...
    -    $this->assertIdentical(array_keys($all_views), array_keys(Views::getViewsAsOptions(TRUE)), 'Expected option keys for all views were returned.');
    +    $this->assertIdenticalWithSafeStrings(array_keys($all_views), array_keys(Views::getViewsAsOptions(TRUE)), 'Expected option keys for all views were returned.');
    

    Why is these necessary? Array key can never be an object..

  • +++ b/core/modules/simpletest/src/TestBase.php
    @@ -750,6 +750,38 @@ protected function assertIdentical($first, $second, $message = '', $group = 'Oth
    +   * @return
    +   *   TRUE if the assertion succeeded, FALSE otherwise.
    +   *
    

    This should be @return bool

stefan.r’s picture

Nice catch @lauriii, let's remove those as it'd just be confusing. That file had a search & replace for consistency back when we did assertEqual.

joelpittet’s picture

StatusFileSize
new31.81 KB
new3.46 KB

Fixes #24 thanks @lauriii

lauriii’s picture

Status: Needs review » Reviewed & tested by the community

This is good for me. I tested all the changed tests locally with the fix given in #2557113: Make t() return a TranslationWrapper object to remove reliance on a static, unpredictable safe list and a little custom logic so that it would fail if assertIdenticalWithSafeStrings is called for no reason.

  1. +++ b/core/modules/simpletest/src/TestBase.php
    @@ -654,7 +654,7 @@ protected function assertNotNull($value, $message = '', $group = 'Other') {
    -   * @return
    +   * @return bool
    
    @@ -750,6 +750,38 @@ protected function assertIdentical($first, $second, $message = '', $group = 'Oth
    +   * @return
    

    I guess bool was added to wrong place. Can be fixed on commit

  2. +++ b/core/modules/locale/src/Tests/LocaleImportFunctionalTest.php
    @@ -238,8 +238,10 @@ public function testLanguageContext() {
    +    // We cast the return value of t() to string so as to retrieve the translated
    +    // value, rendered as a string.
    

    This is over 80chars. Can be also fixed on commit.

joelpittet’s picture

StatusFileSize
new2.09 KB
new31.43 KB

Whoops, fixed that too. Thanks @lauriii

lauriii’s picture

Changes looks good so still RTBC :)

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 28: use-2566447-28.patch, failed testing.

Status: Needs work » Needs review

lauriii queued 28: use-2566447-28.patch for re-testing.

lauriii’s picture

Status: Needs review » Reviewed & tested by the community

Unrelated test fail. Drupal\user\Tests\UserCancelTest has GET http://ec2-54-191-110-230.us-west-2.compute.amazonaws.com/checkout/user/... returned 0 (0 bytes).

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

I'm not sure about this. Especially adding a method calling assertIdenticalAnything that actually doesn't assert identical-ness.

Discussed quickly with @xjm who said "it is a hack for bad tests"

I'm trying to think of alternatives.

stefan.r’s picture

Having already thought about this a fair bit I think we should either keep it and rename it to something more "correct" or introduce another helper.

The helper is a hack, but it's a hack to work around PHP limitations and for developer convenience, where they want to compare an expected and an actual value without too much hassle, in cases where a simple equality check is not enough because of PHP, such as when the array order matters or type conversions matter.

The problem we mostly have is that when comparing two arrays we aren't actually interested in checking "identicalness" per se, but equality is not enough either. So we check for identicalness anyway because it does the job and meets our needs... as long as one of the arrays doesnt contain any safe string objects (which conceptually are no different than strings in /all/ of these comparisons).

So we should have a helper that caters to this use case - if not this helper, renamed, then another one. This will help us in writing future tests, as well as help contrib developers in test conversions (due to the parent) when they had used similar identicalness checks for the same reasons.

dawehner’s picture

I just had a look at many of those examples ...

+++ b/core/modules/block_content/src/Tests/BlockContentListViewsTest.php
@@ -45,10 +45,10 @@ public function testListing() {
     $expected_items = [t('Block description'), t('Block type'), t('Updated'), t('Operations')];
     foreach ($elements as $key => $element) {
       if ($element->xpath('a')) {
-        $this->assertIdentical(trim((string) $element->xpath('a')[0]), $expected_items[$key]);
+        $this->assertIdenticalWithSafeStrings(trim((string) $element->xpath('a')[0]), $expected_items[$key]);
       }
       else {
-        $this->assertIdentical(trim((string) $element[0]), $expected_items[$key]);
+        $this->assertIdenticalWithSafeStrings(trim((string) $element[0]), $expected_items[$key]);
       }
     }

So for example this case. Why do we care about identicalness at all? IMHO we care about equalness, in which case you can define a comparator for your own, in which the t() and ((string) t()) are equal.

stefan.r’s picture

Yes, in most of those cases assertEqual() may have sufficed as well, even though PHP will say that ('foo' == TRUE) and (['foo' => 1, 'bar' => 1] == ['bar' => 1, 'foo' => 1]) and such.

We tried that in #8 and later but then we get into discussions about where we lose test coverage (as == is less strict), it makes the patch harder to review and it makes conversions for contrib harder as well, as it's more convenient to just change a function call where we compare something with t() in it than to make the larger effort of fixing the actual test.

alexpott’s picture

Status: Needs work » Needs review
StatusFileSize
new33.47 KB
new35.44 KB

I think we can fix TestBase::castSafeStrings() to help us. And make it the responsibility of the test to call it. Also we should replace assertIdentical where it just is not necessary - for example - single element arrays where order is just not an issue.

stefan.r’s picture

That'll work as well, it's more explicit about where we are casting.

stefan.r’s picture

Title: Use assertIdenticalWithSafeStrings() instead of assertIdentical() on values that include safe string objects » Cast safe string objects to string or use assertEqual() in assertIdentical() comparisons

Gave this a closer review and this looks fine to me, those equality conversions didn't need the identical-ness check as far as I can see.

My only remark is that in a lot of these cases $expected isn't really the value we expect to be identical to $actual (as we do with other helpers), as $expected isn't really the value we're expecting, i.e. we're doing (f($expected) === f($actual)) and ($expected !== $actual). But I think we can live with this, it's quite clear what we're doing.

stefan.r’s picture

Title: Cast safe string objects to string or use assertEqual() in assertIdentical() comparisons » Cast safe string objects to string or use assertEqual() in some assertIdentical() comparisons
alexpott’s picture

dawehner’s picture

  1. +++ b/core/modules/block_content/src/Tests/BlockContentListViewsTest.php
    @@ -42,7 +42,7 @@ public function testListing() {
    -    $expected_items = [t('Block description'), t('Block type'), t('Updated'), t('Operations')];
    +    $expected_items = ['Block description', 'Block type', 'Updated', 'Operations'];
    

    +1, I mean we don't have locale module enabled on those tests anyway

  2. +++ b/core/modules/config/src/Tests/ConfigImportRenameValidationTest.php
    @@ -112,7 +112,7 @@ public function testRenameValidation() {
    -      $this->assertIdentical($expected, $this->configImporter->getErrors());
    +      $this->assertEqual($expected, $this->configImporter->getErrors());
    
    @@ -155,7 +155,7 @@ public function testRenameSimpleConfigValidation() {
    -      $this->assertIdentical($expected, $this->configImporter->getErrors());
    +      $this->assertEqual($expected, $this->configImporter->getErrors());
    

    Isn't the point that this works simply because we don't have a translation wrapper for t() in place yet? @see core/lib/Drupal/Core/Config/ConfigImporter.php:871 for example

  3. +++ b/core/modules/node/src/Tests/Migrate/d6/MigrateNodeBuilderTest.php
    @@ -34,8 +34,8 @@ class MigrateNodeBuilderTest extends MigrateDrupal6TestBase {
    -    $this->assertIdentical($id, $migration->Id());
    ...
    +    $this->assertEqual($id, $migration->id());
    

    Nitpick: I think we don't need this particular change, right?

  4. +++ b/core/modules/simpletest/src/TestBase.php
    @@ -691,37 +690,26 @@ protected function assertNotEqual($first, $second, $message = '', $group = 'Othe
    +  protected function castSafeStrings($value) {
    

    What about expanding the unit test coverage here? Ideally we would have that particular method available as trait, so the other tests could profit from it as well

stefan.r’s picture

StatusFileSize
new2.8 KB
new36.56 KB

1. Good!
2. assertEqual() ought to be fine here, right?
3. Reverted.
4. Added a AssertionHelperTrait for use in KernelTestBase as well as TestBase, leaving this in the same namespace as AssertContentTrait.

stefan.r’s picture

StatusFileSize
new551 bytes
new36.89 KB
dawehner’s picture

2. assertEqual() ought to be fine here, right?

Ah you are totally right about that.

The last submitted patch, 43: 2566447-43.patch, failed testing.

joelpittet’s picture

Status: Needs review » Reviewed & tested by the community

Had a review through the test changes and everything seems copacetic with me.

Casting seems a bit more explicit which is nice and leaves most of the identicals alone.

dawehner’s picture

  1. +++ b/core/modules/breakpoint/src/Tests/BreakpointDiscoveryTest.php
    @@ -193,7 +193,7 @@ public function testBreakpointGroups() {
         $breakpoint_groups = \Drupal::service('breakpoint.manager')->getGroups();
         // Ensure the order is as expected. Should be sorted by label.
    -    $this->assertIdentical($expected, $breakpoint_groups);
    +    $this->assertIdentical($expected, $this->castSafeStrings($breakpoint_groups));
     
    

    While this for example certainly works, it feels wrong, because do we actually care about the identicalness (this is just one random example), or are actually just interested in the equalness. I totally agree that this is NOT the default of this patch, but I think we need to think more about these things. Some code probably just has it because it is more strict, without thinking whether its actually needed.

  2. +++ b/core/tests/Drupal/KernelTests/KernelTestBase.php
    @@ -20,6 +20,7 @@
     use Drupal\simpletest\AssertContentTrait;
    +use Drupal\simpletest\AssertionHelperTrait;
     use Drupal\simpletest\RandomGeneratorTrait;
    

    Thank you

alexpott’s picture

Status: Reviewed & tested by the community » Needs review
StatusFileSize
new3.53 KB
new38.5 KB
+++ b/core/modules/simpletest/src/AssertionHelperTrait.php
@@ -0,0 +1,38 @@
+/**
+ * @file
+ * Contains \Drupal\simpletest\AssertionHelperTrait.
+ */
+
+namespace Drupal\simpletest;
+
+/**
+ * Provides helper methods for assertions.
+ */
+trait AssertionHelperTrait {
...
+    if ($value instanceof SafeStringInterface) {

No use for SafeStringInterface so this does not work. Also AssertContentTrait and AssertionHelperTrait seems messy so renamed this to AssertHelperTrait. Added unit tests too.

stefan.r’s picture

#49 looks great!

lauriii’s picture

Status: Needs review » Reviewed & tested by the community

Changes in #49 looks good. Also +1 for the new trait and the new version of castSafeStrings().

stefan.r’s picture

Status: Reviewed & tested by the community » Needs work

return $this->assert($this->castSafeStrings($first) == $this->castSafeStrings($second), $message ? $message : SafeMarkup::format('Value @first is equal to value @second.', array('@first' => var_export($first, TRUE), '@second' => var_export($second, TRUE))), $group);

Tested this in the parent and the var_export with TranslationWrappers in it causes problems. We'll need to have castSafeStrings() applied $first and $second there as well. If I get a green patch there I'll upload revised one here.

alexpott’s picture

Status: Needs work » Needs review
StatusFileSize
new38.89 KB
new1.15 KB

Okay fixed.

lauriii’s picture

+++ b/core/modules/simpletest/src/TestBase.php
@@ -659,9 +659,11 @@ protected function assertNotNull($value, $message = '', $group = 'Other') {
+    $first = $this->castSafeStrings($first);
+    $second = $this->castSafeStrings($second);
     return $this->assert($this->castSafeStrings($first) == $this->castSafeStrings($second), $message ? $message : SafeMarkup::format('Value @first is equal to value @second.', array('@first' => var_export($first, TRUE), '@second' => var_export($second, TRUE))), $group);

Shouldn't we remove the second castSafeStrings call from the assertion?

stefan.r’s picture

#54 yes...

This may arguably out of scope here but let's see if we can fix DerivativeTest and InspectionTest here as well? It's adding a bit of noise in the parent issue and the assertEqual()/assertIdentical() calls have a problem with the TranslationWrappers introduced in Drupal\plugin_test\Plugin\MockBlockManager

stefan.r’s picture

StatusFileSize
new40.14 KB
new2.21 KB

Sticking to just casting the labels in the block definitions. The t() in the ContextDefinitions for MockBlockManager also pose a problem but let's fix those in the parent. Tests are green so I think we're good to go here...

joelpittet’s picture

Status: Needs review » Reviewed & tested by the community

Back to RTBC

xjm’s picture

That approach seems much better to me too.

wim leers’s picture

This looks great! And much better than earlier approaches indeed. Nothing to remark.

effulgentsia’s picture

I dislike that we're making tests less strict though, but if it's done for The Better Good overall, then +1.

I agree with both counts. In general, I don't like assertEqual(). PHPUnit's assertEquals() is better in that at least it corrects for 0 == 'foo' problems, but Simpletest's assertEqual(), which other than fixing for safe strings comes with all of PHP's == faults, is icky.

But, I looked over all the ones in this patch and they all seem fine, so in the grand scheme of things, definitely better to unblock #2557113: Make t() return a TranslationWrapper object to remove reliance on a static, unpredictable safe list, so I'll commit this shortly. First though, checking the credit boxes for @lauriii and @dawehner for their substantive reviews.

If anyone at any point is inspired to open issues to change any of these or other assertEqual() assertions to something stricter, please go for it.

effulgentsia’s picture

Status: Reviewed & tested by the community » Fixed

Pushed to 8.0.x. Thanks for the great work on this and the careful reviews!

  • effulgentsia committed 7dee9f8 on 8.0.x
    Issue #2566447 by stefan.r, joelpittet, alexpott, lauriii, dawehner:...
xjm’s picture

I added this issue to the issues for https://www.drupal.org/node/2564451 -- can we update the third item there with a few different examples?

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.