PHPUnit: Getting equalTo assertion to ignore property
Asked Answered
V

6

6

I have the following PHPUnit testcase:

    $mailer = $this->getMockBuilder(MailerInterface::class)->getMock();
    $simpleMailer = new SimpleMailer($mailer);

    $message = (new Message())
        ->setTo($user)
        ->setFrom($from)
        ->setSubject($subject)
        ->setTemplate(SimpleMailer::TEMPLATE)
        ->setContext(['message' => $body]);

    if ($bcc) { $message->addBcc($bcc); }

    $mailer
        ->expects($this->once())
        ->method('send')
        ->with($this->equalTo($message));

    $simpleMailer->sendMessage($user, $subject, $body, $from, $bcc);

This was working fine until the Message class was changed. The Message class now sets a unique ID on construction, meaning that equalTo now returns false with the following difference:

 MailerBundle\Document\Message Object (
-    'id' => '5a372f3c-a8a9-4e1e-913f-d756244c8e52'
+    'id' => '11176427-7d74-4a3c-8708-0026ae666f8b'
     'type' => null
     'user' => Tests\TestUser Object (...)
     'toName' => ''
     'toAddress' => null
     'domain' => null
     'fromName' => null
     'fromAddress' => '[email protected]'
     'bccAddresses' => Array (...)
     'subject' => 'subject'
     'textBody' => null
     'htmlBody' => null
     'template' => 'MailerBundle:MailTemplates:...l.twig'
     'context' => Array (...)
)

Is there any way that I can exclude certain properties from the equality check?

Vetchling answered 24/4, 2017 at 13:32 Comment(4)
Maybe you can override the id for the test!?Loincloth
The id is a private property with a public getter method. Exposing the setter only for the tests doesn't sound like a good idea to me. I'd like to find a solution without changing code outside the tests.Vetchling
If you have php7 you can do it with an anonym class like new class extends Message { public function _construct(){ /*override here*/}}, no need to override the original. And btw you can mock the getter, maybe that helpsLoincloth
Yes, I could influence the Message I construct in the tests. But not the message constructed in simpleMailer->sendMessageVetchling
D
2

Another solution using a custom comparator as suggested by Sebastian in https://github.com/sebastianbergmann/phpunit/issues/4034

My use case is to assert objects for equality after the test subject has been unserialize(serialize()): A custom __serialize() ignores some properties when doing that, so a custom comparator is used to ignore these.

final class FooTest extends TestCase
{
    public function setUp(): void
    {
        parent::setUp();
        // Register custom comparator to compare IncludeTree with its unserialize(serialize())
        // representation since we don't serialize all properties. Custom comparators are
        // unregistered after test by phpunit runBare() automatically.
        $this->registerComparator(new UnserializedIncludeTreeObjectComparator());
    }

    /**
     * @test
     */
    public function foo(): void
    {
        $includeTree = [...]
        self::assertEquals($includeTree, unserialize(serialize($includeTree)));
    }
}

With this comparator class:

final class UnserializedIncludeTreeObjectComparator extends ObjectComparator
{
    protected function toArray($object): array
    {
        $arrayRepresentation = parent::toArray($object);
        if ($object instanceof IncludeInterface) {
            // This property is not serialized and falls back to default
            // value on unserialize(). We ignore it for equality comparison.
            unset($arrayRepresentation['name']);
        }
        return $arrayRepresentation;
    }
}
Deceptive answered 19/10, 2022 at 9:32 Comment(1)
That actually looks like the most natural solution. Thank you.Vetchling
N
4

If your Message class has getters, you can use a callback in with function to match only properties you care about. Something similar to

$mailer
    ->expects($this->once())
    ->method('send')
    ->with($this->callback(function(Message $message) use ($user, $from, $subject, $body) {
            return $message->getTo() == $user 
                && $message->getFrom() == $from 
                && $message->getSubject() == $subject 
                && $message->getTemplate() == SimpleMailer::TEMPLATE
                && $message->getContext()['message'] == $body
        }));
Nadene answered 24/4, 2017 at 13:48 Comment(2)
Thank you! This seems to be the most straight forward way to solve the problem.Vetchling
Having to add every new property to the test is just a useless waste of time.Dayna
I
2

I just wrote something similar, and was curious if I just reinvented the wheel (I pretty much did). However, I thought would share my result here in case it helps anyone, since it matches your use case like mine.

My goal was to compare two objects and assert equality between properties, with the ability to ignore specified keys. I'm using this with the current PHPunit (Feb 2020).

private function assertEqualWithIgnore($expected, $actual, $ignoreKeys = [], $currentKey = null): void
    {
        if (is_object($expected)) {
            foreach ($expected as $key => $value) {
                $this->assertEqualWithIgnore($expected->$key, $actual->$key, $ignoreKeys, $key);
            }
        } elseif (is_array($expected)) {
            foreach ($expected as $key => $value) {
                $this->assertEqualWithIgnore($expected[$key], $actual[$key], $ignoreKeys, $key);
            }
        } elseif ($currentKey !== null && !in_array($currentKey, $ignoreKeys)) {
            $this->assertEquals($expected, $actual);
        }
    }
Implication answered 7/2, 2020 at 23:37 Comment(0)
S
2

Inspired by Brian's answer, here is an helper that trim the unwanted ids before making an equality check. This way the behavior remains very close to phpunit's assertEquals.

You get the equality checks on all properties at once instead of one at the time. You can also more easily spot where the check is failing.

private function assertEqualsButIgnore($expected, $actual, $ignoreKeys = []): void
{
    $copyExpected = $expected;
    $this->recursiveUnset($copyExpected, $ignoreKeys);

    $copyActual = $actual;
    $this->recursiveUnset($copyActual, $ignoreKeys);

    $this->assertEquals($copyExpected, $copyActual);
}

private function recursiveUnset(&$objOrArray, $unwanted_key): void
{
    foreach($unwanted_key as $key) {
        if(is_array($objOrArray)) {
            unset($objOrArray[$key]);
        } else {
            unset($objOrArray->$key);
        }
    }
    foreach ($objOrArray as &$value) {
        if (is_array($value) || is_object(($value))) {
            $this->recursiveUnset($value, $unwanted_key);
        }
    }
}
Steffie answered 1/4, 2020 at 20:47 Comment(0)
D
2

Another solution using a custom comparator as suggested by Sebastian in https://github.com/sebastianbergmann/phpunit/issues/4034

My use case is to assert objects for equality after the test subject has been unserialize(serialize()): A custom __serialize() ignores some properties when doing that, so a custom comparator is used to ignore these.

final class FooTest extends TestCase
{
    public function setUp(): void
    {
        parent::setUp();
        // Register custom comparator to compare IncludeTree with its unserialize(serialize())
        // representation since we don't serialize all properties. Custom comparators are
        // unregistered after test by phpunit runBare() automatically.
        $this->registerComparator(new UnserializedIncludeTreeObjectComparator());
    }

    /**
     * @test
     */
    public function foo(): void
    {
        $includeTree = [...]
        self::assertEquals($includeTree, unserialize(serialize($includeTree)));
    }
}

With this comparator class:

final class UnserializedIncludeTreeObjectComparator extends ObjectComparator
{
    protected function toArray($object): array
    {
        $arrayRepresentation = parent::toArray($object);
        if ($object instanceof IncludeInterface) {
            // This property is not serialized and falls back to default
            // value on unserialize(). We ignore it for equality comparison.
            unset($arrayRepresentation['name']);
        }
        return $arrayRepresentation;
    }
}
Deceptive answered 19/10, 2022 at 9:32 Comment(1)
That actually looks like the most natural solution. Thank you.Vetchling
D
1

Why not just...

$exporter = SebastianBergmann\Exporter\Exporter();

$expectedObjectAsArray = $exporter->toArray($expectedObject);
$actualObjectAsArray = $exporter->toArray($actualObjectAsArray);

unset($expectedObjectAsArray['unwantedProperty']);
unset($actualObjectAsArray['unwantedProperty']);

$this->assertEquals($expectedObjectAsArray, $actualObjectAsArray);

PhpUnit already compares objects as arrays:

// vendor/sebastian/comparator/src/ObjectComparator.php:77
parent::assertEquals(
    $this->toArray($expected),
    $this->toArray($actual),
    $delta,
    $canonicalize,
    $ignoreCase,
    $processed
);   

My proposed solution is therefore just as performant as comparing via PhpUnit.

Dayna answered 13/11, 2023 at 16:52 Comment(0)
M
0

Do you have to make sure it's the equal object? If not I would recommend using a matcher, e.g.

->with($this->isInstanceOf(Message::class))

You could also combine multiple checks using e.g. logicalAnd()

Here is a good resource of available matchers: http://archive.gregk.me/2011/phpunit-with-method-argument-matchers/

Mistassini answered 24/4, 2017 at 13:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.