'phpunit - How to mock global function to throw an exception?

I am writing phpunit tests for my app and one of the units under test uses the following function:

private function createRandomString(): string {
    try {
        return bin2hex(random_bytes(32));
    } catch (Exception) {
        $this->logger->warning('Unable to created random binary bytes.');
    }
    return substr(str_shuffle('0123456789abcdefABCDEF'), 10, 64);
}

The random_bytes function that is included in PHP 7+, may throw an exception, so this method has the "fallback" random string generation in case of problems. I would like to test the problem scenario, so I need the exception to be thrown, but none of the methods I found properly mock this function (I also tried to do the same for bin2hex, but again with no positive result). I have already tried:

  • MockBuilder
$builder = new MockBuilder();
$builder->setNamespace(__NAMESPACE__)
    ->setName('random_bytes')
    ->setFunction(fn() => throw new Exception());

$mock = $builder->build();
$mock->enable();
  • FunctionMock
$mock = $this->getFunctionMock(__NAMESPACE__, 'random_bytes');
$mock->expects($this->once())->willThrowException(new Exception());
  • Spy
$spy = new Spy(__NAMESPACE__, 'random_bytes', fn() => throw new Exception());
$spy->enable();

Is there any way of mocking this function? I use pure phpunit and php-mock (not Mockery), because I needed this configuration for other tests.



Solution 1:[1]

Ok, I found some solution/workaround.

I decided to do the Dependency Injection. I created another service that was easy to mock in the typical way.

class RandomizerService {
    private const AVAILABLE_CHARS = '0123456789abcdefABCDEF';

    /**
     * @throws Exception
     */
    public function getRandomBytes(int $length): string {
        return random_bytes($length);
    }

    public function getRandomString(int $length): string {
        return substr(str_shuffle(self::AVAILABLE_CHARS), 10, 64);
    }
}

And used it as follows:

private function createRandomString(): string {
    try {
        return bin2hex($this->randomizerService->getRandomBytes(32));
    } catch (Exception) {
        $this->logger->warning('Unable to created random binary bytes.');
    }
    return $this->randomizerService->getRandomString(64);
}

Then I only mocked it:

$randomizerService->method('getRandomBytes')
    ->willThrowException(new Exception());
$randomizerService->method('getRandomString')
    ->willReturn('abcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefgh');

Or if I wanted no mocks, I created it instance:

$randomizerService = new RandomizerService();

And passed the object or mock as a constructor parameter.

Now everything works and it's covered in 100%.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1