Troubleshoot a Symfony ClockMock test

Photo by Djim Loic on Unsplash

Troubleshoot a Symfony ClockMock test

Review of known pitfalls to quickly debug a ClockMock test

The Symfony ClockMock is an amazing yet tricky feature to mock date & time in your PHPUnit test suite.

You followed the documentation and yet date and time don't get mocked in your tests?

Let's review how it works and the common pitfalls we know of at Springly.

How it works

The ClockMock relies on PHP name resolution rules to define date & time functions in your app namespace when you run tests annotated @time-sensitive

namespace App\Tests\Something

use PHPUnit\Framework\TestCase;
use Symfony\Component\Stopwatch\Stopwatch;

/**
 * @group time-sensitive
 */
class MyTest extends TestCase
{
    public function testSomething()
    {
        $stopwatch = new Stopwatch();

        $stopwatch->start('event_name');
        sleep(10);
        $duration = $stopwatch->stop('event_name')->getDuration();

        self::assertEquals(10000, $duration);
    }
}

The following functions are defined with the previous code block:

  • App\Tests\Something\time() & App\Something\time()
  • App\Tests\Something\microtime() & App\Something\microtime()
  • App\Tests\Something\sleep() & App\Something\sleep()
  • App\Tests\Something\usleep() & App\Something\usleep()
  • App\Tests\Something\date() & App\Something\date()
  • App\Tests\Something\gmdate() & App\Something\gmdate()

For the most curious, this is done in Symfony\Bridge\PhpUnit\ClockMock with an eval() call.

Common pitfalls

Use the right binary

ClockMock only works if you run your test suite with the vendor/bin/simple-phpunit binary.

Don't use Composer vendor/autoload.php or the base vendor/bin/phpunit binary when running tests. For instance, the right setup with Docker & PHPStorm is image.png

Alternatively, you can also use this configuration to explicitly set up the right listener:

<!-- phpunit.xml.dist -->
<!-- ... -->
<listeners>
    <listener class="\Symfony\Bridge\PhpUnit\SymfonyTestsListener"/>
</listeners>

Not every time classes and functions are mocked

Only the following functions are mockable:

  • time()
  • microtime()
  • sleep()
  • usleep()
  • date()
  • gmdate()

The DateTime class is not mockable nor other PHP classes.

However, you can use this as a workaround:

$real = new DateTime('now');
$mocked = new DateTime('@' . time());

Fully qualified function name are not mocked

Time-based function mocking follows the PHP namespace resolutions rules so "fully qualified function calls" (e.g \time()) cannot be mocked.

Functions are mocked in a limited set of namespaces

Following the first example, the PHPUnit bridge will mock time functions in these namespaces:

  • App\Something
  • App\Tests\Something

Check that your time functions calls are actually done within one of these mocked namespace or call ClockMock::register(MyClass::class) beforehand.

Workaround PHP internal cache system

Newest versions of PHP cache name resolution results to improve performance as detailed in the following bug reports:

It means that the following test suite won't work as you expect.

namespace App\Tests\Something

use PHPUnit\Framework\TestCase;
use Symfony\Component\Stopwatch\Stopwatch;

class MyTest extends TestCase
{
    public function testSomething()
    {
        $now = time();
        self::assertTrue($now > 0);

        ClockMock::register(MyTest::class);
        ClockMock::withClockMock(0);

        // This will fail as the namespace resolution result was cached
        // by the previous call to time() 
        self::assertEquals(0, time());
    }
}

You can either use a bootstrap.php or a custom PHPUnit configuration to work around it for common used classes in your codebase (like a base Doctrine entity trait to set createdAt & updatedAt properties).

<!-- phpunit.xml.dist -->
<!-- ... -->
<listeners>
<listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener">
  <arguments>
    <array>
      <element key="time-sensitive">
        <string>App\Tests\Something</string>
      </element>
    </array>
  </arguments>
</listener>
</listeners>

or a bootstrap file:

# bootstrap.php to setup in phpunit.xml
ClockMock::register('App\\Something\\');

⚠️ Use the namespace and not the fully qualified class name. If your class is App\Something\MyClass then use App\Something in the example above and not App\Something\MyClass.

Happy testing!