Reputation: 10084
As of new phpunit versions \DateTime objects are compared with microseconds precision. It's not always a good idea, because if I have an object like this:
class QueueItem
{
public function __construct()
{
$this->setCreatedAt(new \DateTime('now', new \DateTimeZone('UTC')));
$this->setUpdatedAt(new \DateTime('now', new \DateTimeZone('UTC')));
}
}
I'll never be able to use assertEquals
for the whole object in my tests, because datetime properties will always be different due to microseconds.
It's not a big deal when just comparing objects (I can compare each field separately). But here comes the problem:
$expectedQueue = [
new QueueItem(),
new QueueItem()
];
$this->repositoryWrite
->expects($this->once())
->method('save')
->with(...$expectedQueue);
with
compares the whole array of the arguments including datetime properties, which I can not predict.
The only solution I've got is using willReturnCallback
and comparing each argument separately:
$this->repositoryWrite
->expects($this->once())
->method('save')
->willReturnCallback(function(...$queue) use ($expectedQueue) {
/** @var QueueItem[] $queue */
foreach ($queue as $k => $queueItem) {
$this->assertEquals($expectedQueue->getSomeProp(), $queueItem[0]->getSomeProp());
}
});
It looks... well, strange. Are there other solution for this one? Can I change comparator's precision for \DateTime objects?
Upvotes: 0
Views: 1381
Reputation: 35139
Testing times can be quite difficult - and with the move to a default high-resolution in PHP7, it did get more 'interesting'. In your first example. it might not be so hard - set a $now = new \DateTime('now', new \DateTimeZone('UTC'));
and set the fields, but that gets a lot harder inside a class that you are testing that you may not have such easy control over.
However, with a little PHP Namespace hacking, you can override (on a limited basis), some of the base language functions, like time()
, and sleep()
. The Symfony component, 'phpunit-bridge', provides a ClockMock class that can override time()
to control the clock - and turn sleep(10)
into a time()+10
call - turning a real-time pause in a test into an almost zero-time event, and making comparisons exact.
It can only do so much, and can't override a DateTime()
constructor, but it's easy enough to replace that with something that will use time()
to set it -- $now = \DateTime::createFromFormat('U', (string) time());
and so be controlled with the ClockMocking.
You can just install the PHPUnit Bridge, and then just use the ClockMock class in your tests (with that little extra use of time() to create new DateTime's).
I'm using the PHPUnit-bridge in my tests, and to enable the Clock Mocking I can just annotate the test class with '@group time-sensitive' (if it's following the usual style of naming the class it is testing with a Test\
prefix and ...Test
classname suffix), and the bridge will recognise that, and automatically enable mocking on the class - or I can start it up manually in the test code for other classes.
Upvotes: 3