Ssss Ppppp
Ssss Ppppp

Reputation: 4404

PHPUnit How to constraint mock stub method input with array subset containing instanceOf?

I want to test pretty specific piece of code, but I can't find a good way to do it. I have such code:

public function foo()
{
    try {
        //...some code
        $this->service->connectUser();
    } catch (\OAuth2Exception $e) {
        $this->logger->error(
            $e->getMessage(),
            ['exception' => $e]
        );
    }
}

And I want to test if the exception was thrown and logged to $this->logger. But I can't find a good way to do it. Here is how I do it currently.

public function testFoo()
{
    $oauthException = new \OAuth2Exception('OAuth2Exception message');

    //This is a $service Mock created with $this->getMockBuilder() in test case injected to AuthManager.
    $this->service
        ->method('connectUser')
        ->will($this->throwException($oauthException));

    //This is a $logger Mock created with $this->getMockBuilder() in test case injected to AuthManager.
    $this->logger
        ->expects($this->once())
        ->method('error')
        ->with(
            $this->isType('string'),
            $this->logicalAnd(
                $this->arrayHasKey('exception'),
                $this->contains($oauthException)
            )
        );

    //AuthManager is the class beeing tested.
    $this->authManager->foo($this->token);
}

This will test if error method was called with certain parameters, but array key 'exception' and exception object can exist in different parts of the array. What I mean is that test will pass for such error method call:

$this->logger->error(
    $e->getMessage(),
    [
        'exception' => 'someValue',
        'someKey' => $e,
    ]
);

I would like to make sure that error method will always receive such subset ['exception' => $e]. Something like this would be perfect:

$this->logger
    ->expects($this->once())
    ->method('error')
    ->with(
        $this->isType('string'),
        $this->arrayHasSubset([
            'exception' => $oauthException,
        ])
    );

Is it possible to achieve with PHPUnit?

Upvotes: 1

Views: 1292

Answers (2)

localheinz
localheinz

Reputation: 9582

You can use the callback() constraint:

public function testFoo()
{
    $exception = new \OAuth2Exception('OAuth2Exception message');

    $this->service
        ->expects($this->once())
        ->method('connectUser')
        ->willThrowException($exception);

    $this->logger
        ->expects($this->once())
        ->method('error')
        ->with(
            $this->identicalTo($exception->getMessage()),
            $this->logicalAnd(
                $this->isType('array'),
                $this->callback(function (array $context) use ($exception) {
                    $expected = [
                        'exception' => $exception,
                    ];

                    $this->assertArraySubset($expected, $context);

                    return true;
                })
            )
        );

    $this->authManager->foo($this->token);
}

See https://phpunit.de/manual/current/en/test-doubles.html#test-doubles.mock-objects:

The callback() constraint can be used for more complex argument verification. This constraint takes a PHP callback as its only argument. The PHP callback will receive the argument to be verified as its only argument and should return true if the argument passes verification and false otherwise.

Also note how I adjusted setting up your test doubles:

  • expect the method connectUser() to be invoked exactly once
  • use $this->willThrowException() instead of $this->will($this->throwException())
  • use $this->identicalTo($exception->getMessage()) instead of the more loose $this->isType('string')

I always try to make an argument to be as specific as possible, and only loosen constraints on intention.

Upvotes: 3

Igor Batov
Igor Batov

Reputation: 26

You can try PHPUnit spies as described in https://lyte.id.au/2014/03/01/spying-with-phpunit/

With spy you can do something like

$this->logger->expects($spy = $this->any())->method('error');
$invocations = $spy->getInvocations();
/**
 * Now $invocations[0]->parameters contains arguments of first invocation.
 */

Upvotes: 0

Related Questions