How can I test a function that uses DateTime to get the current time?
Asked Answered
U

1

5

Most of the answers I have seen on StackOverflow are without using the DateTime object, and are instead using the date() function. This makes them very dirty solutions (overriding date(), mocking a protected function of the subject under test, etc).

Is there a way to mock DateTime, effectively mocking the current date/time?

As an example, here is the code I'd like to test:

public function __construct(UserInterface $user, EntityManager $manager)
{
    $this->user = $user;
    $this->manager = $manager;
}

public function create(Tunnel $tunnel, $chain, $response)
{
    $history = new CommandHistory();

    $history->setTunnel($tunnel)
        ->setCommand($chain)
        ->setResponse($response)
        ->setUser($this->user)
    ;

    $this->manager->persist($history);
    $this->manager->flush();
}

Here is where I set the date and time in my CommandHistory class:

class CommandHistory
{
    // Property definitions...

    public function __construct()
    {
        $this->time = new \DateTime();
    }
}

And here is my unit test:

public function testCreate()
{
    $user = new User();
    $manager = $this->mockManagerWithUser($user);

    $tunnel = $this->tunnel;
    $chain = 'Commands`Chain';
    $response = 'This is the response!';

    $creator = new CommandHistoryCreator($user, $manager);
    $creator->create($tunnel, $chain, $response);
}

protected function mockManagerWithUser(UserInterface $user)
{
    $manager = \Mockery::mock('Doctrine\ORM\EntityManager');

    $manager->shouldReceive('persist')->once()->with(\Mockery::on(function(CommandHistory $argument) use ($user) {
        return
            $argument->getCommand() === 'Commands`Chain'
            && $argument->getResponse() === 'This is the response!'
            && $argument->getTunnel() === $this->tunnel
            && $argument->getUser() === $user
        ;
    }));
    $manager->shouldReceive('flush')->once()->withNoArgs();

    return $manager;
}

As you can see, I've created a rather long-winded closure only to exclude the comparison of the field that contains the current time, and I feel like this is hurting the readability of my test.

Also, to preserve ease of use for people who are using this class, I don't want to have to make them pass in the current time to the create() function. I believe adding strange behavior to my classes only to make them testable means I'm doing something wrong.

Undis answered 9/5, 2015 at 6:52 Comment(2)
Where are you using the current time? I'm not that familiar with PHP but I don't see date or time used anywhere is any of the code above... I assume that its a property of the CommandHistory object, which you don't show...Gertrude
Yes, in the constructor of CommandHistory. I just added that to my question, too. Thanks for the reminder :-)Undis
G
11

So the standard approach to solving this relies on accepting that in your current implementation you have a static, implicit, undeclared dependency on an object which provides the current time (wrapped in a the new instance of the DateTime object). If you did this with your own code (rather than a class from the framework/language) you would not be able to test easily either.

The solution is to stop using the implicit undeclared dependency and declare your implicit dependency explictly. I would do this by creating a DateTimeProvider (or DateTimeFactory) interface which has a method GetCurrentDateTime. Pass this into your constructor for your CommandHistoryCreator and pass it into the CommandHistory constructor. The CommandHistory will then ask the provider to get the current date time object rather than creating a new one itself, and can carry on as it is.

This will allow you to provider a mock DateTime in your tests and check that the CommandHistory is persisted with the correct DateTime

Gertrude answered 9/5, 2015 at 10:42 Comment(3)
Well, the problem with this approach is that making this dependency explicit doesn't solve anything for other developers. They now have to do something that is very unfamiliar in the PHP land, namely passing a DateTimeFactory into the constructor for a PHP entity.Undis
You asked for a solution and got a good one. Tell those developers to test their code and they will need this too... maybe this is unfamiliar due to lack of good code testing.Lincolnlincolnshire
@parhamdoustdar that is why I started my answer with ' relies on accepting that in your current implementation you have a static, implicit, undeclared dependency'. In most languages datetime is like this and in most languages people are not used to providing a factory 'just to get the current time'. If you make the constructor require the interface, people will be able to see that they need it and they will get used to using it.Gertrude

© 2022 - 2024 — McMap. All rights reserved.