phpunit icon indicating copy to clipboard operation
phpunit copied to clipboard

Assert that arrays (or objects) are equal while excluding certain keys (or properties)

Open NolwennD opened this issue 3 years ago • 6 comments

Some object may have properties with unpredictable values, like random UUID, and you want to compare the predictable ones.

In Java, with AssertJ it’s possible with: assertThat(actual).usingRecursiveComparison().ignoringFields("id").isEqualTo(expected).

Maybe a signature like assertEqualsIgnoringFields($expected, $actual, string ...$fields)

<?php

class Foo {

    private readonly DateTimeImmutable $date;
    public function __construct(private readonly string $name)
    {
        $this->date = new DateTimeImmutable();
    }
}

/**
* @test
*/
public function passingTest(): void
{
    $foo1 = new Foo("fooName");
    $foo2 = new Foo("fooName");

    self::assertEqualsIgnoringFields($foo1, $foo2, "date");
}

NolwennD avatar Nov 30 '22 00:11 NolwennD

My suggestion would be to implement Foo::equals() and then use assertObjectEquals().

sebastianbergmann avatar Jan 25 '23 06:01 sebastianbergmann

This can be done with a custom comparator, if I understand the issue correctly. They are surprisingly simple to implement, see https://stackoverflow.com/questions/43589561/phpunit-getting-equalto-assertion-to-ignore-property for an example that worked for me.

lolli42 avatar Feb 05 '23 11:02 lolli42

@lolli42 You are correct. I am still undecided whether or not I want to implement this or not.

sebastianbergmann avatar Feb 05 '23 16:02 sebastianbergmann

I have quite a complex immutable value object representing a time interval (like 2023-09-19T16:43:12/2023-09-19T23:59). Some of the methods (isEmpty, getInclusiveEnd, etc) have quite an impact on performances if they are called a big number of times.

So I thought: let's add a private property that acts as a cache for these values (as they can never change for a given instance of the object), and they'll be computed only once the first time we need them. Like :

private bool $isEmpty;

public function isEmpty(): bool
{
    return $this->isEmpty ??=
        null !== $this->start && null !== $this->end && $this->start->equals($this->end);
}

What I did not expect was that now most of my unit tests are broken, because we compare objects (and array of objects, and objects containing arrays of objects,... you get the gist) quite a lot, and now sometimes these cache properties will be set, and sometimes they wont (depending on whether or not the method was called at least once before the comparison). Which is then seen as a difference by assertEquals/assertSame :

Failed asserting that two objects are equal.
--- Expected
+++ Actual
@@ @@
             0 => Gammadia\DateTimeExtra\LocalDateTimeInterval Object (
                 'start' => Brick\DateTime\LocalDateTime Object (...)
                 'end' => Brick\DateTime\LocalDateTime Object (...)
-                'isEmpty' => false
             )
             1 => 1
         )
     )
 )

@sebastianbergmann Is there a way to tell PHPUnit to globally ignore these cache-related private properties when doing comparisons (so not by registering some custom comparator in each test or changing the assert method) ? Something like the #[Exclude] PHP attribute in Symfony Serializer or such ?

Or is there another way to have this cache without the comparison issue that I'm not seeing ?

gnutix avatar Sep 19 '23 14:09 gnutix

No, you "only" have the two options PHPUnit provides for this so far: registering a custom comparator or using assertObjectEquals() which in turn uses your value object's equals() or equalTo() method. IMO, the latter is preferable over the former.

sebastianbergmann avatar Sep 19 '23 14:09 sebastianbergmann

Thanks for your answer. I went the assertObjectEquals road. I had to write a quite weird implementation of equals on another object that uses LocalDateTimeInterval internally, but otherwise it went okay and works.

gnutix avatar Sep 19 '23 16:09 gnutix