Can I "Mock" time in PHPUnit?
Asked Answered
C

15

82

... not knowing if 'mock' is the right word.

Anyway, I have an inherited code-base that I'm trying to write some tests for that are time-based. Trying not to be too vague, the code is related to looking at the history of an item and determining if that item has now based a time threshold.

At some point I also need to test adding something to that history and checking that the threshold is now changed (and, obviously, correct).

The problem that I'm hitting is that part of the code I'm testing is using calls to time() and so I'm finding it really hard to know exactly what the threshold time should be, based on the fact that I'm not quite sure exactly when that time() function is going to be called.

So my question is basically this: is there some way for me to 'override' the time() call, or somehow 'mock out' the time, such that my tests are working in a 'known time'?

Or do I just have to accept the fact that I'm going to have to do something in the code that I'm testing, to somehow allow me to force it to use a particular time if need be?

Either way, are there any 'common practices' for developing time-sensitive functionality that is test friendly?

Edit: Part of my problem, too, is the fact that the time that things occurred in history affect the threshold. Here's an example of part of my problem...

Imagine you have a banana and you're trying to work out when it needs to be eaten by. Let's say that it will expire within 3 days, unless it was sprayed with some chemical, in which case we add 4 days to the expiry, from the time the spray was applied. Then, we can add another 3 months to it by freezing it, but if it's been frozen then we only have 1 day to use it after it thaws.

All of these rules are dictated by historical timings. I agree that I could use the Dominik's suggestion of testing within a few seconds, but what of my historical data? Should I just 'create' that on the fly?

As you may or may not be able to tell, I'm still trying to get a hang of all of this 'testing' concept ;)

Cochise answered 3/3, 2010 at 14:2 Comment(1)
For PHP7 you could use github.com/runkit7/Timecop-PHP which is based on runkit7Twinberry
W
64

I recently came up with another solution that is great if you are using PHP 5.3 namespaces. You can implement a new time() function inside your current namespace and create a shared resource where you set the return value in your tests. Then any unqualified call to time() will use your new function.

For further reading I described it in detail in my blog

Worldly answered 17/3, 2011 at 10:12 Comment(6)
I really like this idea Fabian. An added benefit is that it forces my workmates to upgrade to 5.3 ;)Cochise
This is extremely useful, thank you for sharing this technique with us Fabian - much appreciated!Decalcify
genius work here. Namespaces for functions does make it useful to substitute built-in functions for testing.Succuss
I recently implemented the library php-mock which uses that language feature for mocking non deterministic PHP functions like time().Highoctane
Great solution. Even if your SUT is in a different namespace than your test, you can use it by using "multiple namespaces in the same file" php.net/manual/en/language.namespaces.definitionmultiple.phpLeucas
That, my friend, is an awesome idea. It solves several of my testing issues!Murvyn
W
11

Carbon::setTestNow(Carbon $time = null) makes any call to Carbon::now() or new Carbon('now') return the same time.

https://medium.com/@stefanledin/mock-date-and-time-with-carbon-8a9f72cb843d

Example:

    public function testSomething()
    {
        $now = Carbon::now();
        // Mock Carbon::now() / new Carbon('now') to always return the same time
        Carbon::setTestNow($now);

        // Do the time sensitive test:
        $this->retroEncabulator('prefabulate')
            ->assertJsonFragment(['whenDidThisHappen' => $now->timestamp])

        // Release the Carbon::now() mock
        Carbon::setTestNow();
    }

The $this->retroEncabulator() function needs to use Carbon::now() or new Carbon('now') internally of course.

Wheelman answered 6/5, 2020 at 13:0 Comment(2)
Maybe you can use it like this, $now = Carbon::now(); Carbon::setTestNow($now);Selaginella
@mohammad.kaab I added an example doing exactly that 👍Wheelman
P
10

For those of you working with symfony (>= 2.8): Symfony's PHPUnit Bridge includes a ClockMock feature that overrides the built-in methods time, microtime, sleep and usleep.

See: http://symfony.com/doc/2.8/components/phpunit_bridge.html#clock-mocking

Pathan answered 17/12, 2017 at 16:34 Comment(1)
Thanks, that help a lot!Heliostat
L
6

You can mock time for test using Clock from ouzo-goodies. (Disclaimer: I wrote this library.)

In code use simply:

$time = Clock::now();

Then in tests:

Clock::freeze('2014-01-07 12:34');
$result = Class::getCurrDate();
$this->assertEquals('2014-01-07', $result);
Luik answered 7/1, 2015 at 19:1 Comment(1)
When you link to your own software, you should include a disclaimer letting everyone know you wrote it.Holoblastic
A
5

I had to simulate a particular request in future and past date in the app itself (not in Unit Tests). Hence all calls to \DateTime::now() should return the date that was previously set throughout the app.

I decided to go with this library https://github.com/rezzza/TimeTraveler, since I can mock the dates without altering all the codes.

\Rezzza\TimeTraveler::enable();
\Rezzza\TimeTraveler::moveTo('2011-06-10 11:00:00');

var_dump(new \DateTime());           // 2011-06-10 11:00:00
var_dump(new \DateTime('+2 hours')); // 2011-06-10 13:00:00
Ariannearianrhod answered 6/9, 2015 at 1:21 Comment(1)
it seems cool for the one that are using a new \DateTime() in their code. But How is this supposed to be installed? No info in the github repo.Superfluous
E
3

Personally, I keep using time() in the tested functions/methods. In your test code, just make sure to not test for equality with time(), but simply for a time difference of less than 1 or 2 (depending on how much time the function takes to execute)

Emotive answered 3/3, 2010 at 14:11 Comment(3)
At the moment it looks like that's the way I'll have to go. I've added an 'example' to my problem, too, in case that helps. Thanks.Cochise
I saw your example. Again, personally, for this kind of tests I use the phpunit setup method to prepare the 'correct' historical data (for example inside the database)Emotive
This will make your tests very fragile. They may fail apparently without reason, whenever the process under test experiences a delay (for whatever reason).Lewislewisite
D
2

You can overide php's time() function using the runkit extension. Make sure you set runkit.internal_overide to On

Disfeature answered 23/4, 2012 at 13:56 Comment(0)
B
2

Using [runkit][1] extension:

define('MOCK_DATE', '2014-01-08');
define('MOCK_TIME', '17:30:00');
define('MOCK_DATETIME', MOCK_DATE.' '.MOCK_TIME);

private function mockDate()
{
    runkit_function_rename('date', 'date_real');
    runkit_function_add('date','$format="Y-m-d H:i:s", $timestamp=NULL', '$ts = $timestamp ? $timestamp : strtotime(MOCK_DATETIME); return date_real($format, $ts);');
}


private function unmockDate()
{
    runkit_function_remove('date');
    runkit_function_rename('date_real', 'date');
}

You can even test the mock like this:

public function testMockDate()
{
    $this->mockDate();
    $this->assertEquals(MOCK_DATE, date('Y-m-d'));
    $this->assertEquals(MOCK_TIME, date('H:i:s'));
    $this->assertEquals(MOCK_DATETIME, date());
    $this->unmockDate();
}
Bromberg answered 27/12, 2013 at 12:23 Comment(0)
E
2

In most cases this will do. It has some advantages:

  • you don't have to mock anything
  • you don't need external plugins
  • you can use any time function, not only time() but DateTime objects as well
  • you don't need to use namespaces.

It's using phpunit, but you can addapt it to any other testing framework, you just need function that works like assertContains() from phpunit.

1) Add below function to your test class or bootstrap. Default tolerance for time is 2 secs. You can change it by passing 3rd argument to assertTimeEquals or modify function args.

private function assertTimeEquals($testedTime, $shouldBeTime, $timeTolerance = 2)
{
    $toleranceRange = range($shouldBeTime, $shouldBeTime+$timeTolerance);
    return $this->assertContains($testedTime, $toleranceRange);
}

2) Testing example:

public function testGetLastLogDateInSecondsAgo()
{
    // given
    $date = new DateTime();
    $date->modify('-189 seconds');

    // when
    $this->setLastLogDate($date);

    // then
    $this->assertTimeEquals(189, $this->userData->getLastLogDateInSecondsAgo());
}

assertTimeEquals() will check if array of (189, 190, 191) contains 189.

This test should be passed for correct working function IF executing test function takes less then 2 seconds.

It's not perfect and super-accurate, but it's very simple and in many cases it's enough to test what you want to test.

Effendi answered 29/9, 2015 at 8:37 Comment(0)
S
1

Simplest solution would be to override PHP time() function and replace it with your own version. However, you cannot replace built-in PHP functions easily (see here).

Short of that, the only way is to abstract time() call to some class/function of your own that would return the time you need for testing.

Alternatively, you could run the test system (operating system) in a virtual machine and change the time of the entire virtual computer.

Shrum answered 3/3, 2010 at 14:56 Comment(1)
Thanks Milan: while I agree that running in a VM would be an option to 'force' the time, I think I'd still have to account for the 'runtime variables' and so still end up doing what Dominik suggested. Interesting idea, though, thanks.Cochise
P
1

Here's an addition to fab's post. I did the namespace based override using an eval. This way, I can just run it for tests and not the rest of my code. I run a function similar to:

function timeOverrides($namespaces = array()) {
  $returnTime = time();
  foreach ($namespaces as $namespace) {
    eval("namespace $namespace; function time() { return $returnTime; }");
  }
}

then pass in timeOverrides(array(...)) in the test setup so that my tests only have to keep track of what namespaces time() is called in.

Pearlpearla answered 7/3, 2014 at 4:26 Comment(0)
M
1

Disclaimer: I wrote this library.

If you are free to install php extensions in your system, you could then use https://github.com/slope-it/clock-mock.

That library requires ext-uopz >= 6.1.1 and by using ClockMock::freeze and ClockMock::reset you can move the internal php clock to whatever date and time you like. The cool thing about it is that it requires zero modifications to your production code because it mocks transparently \DateTime and \DateTimeImmutable objects as well as some of the global functions (e.g. date(), time(), etc...).

Mononucleosis answered 23/5, 2021 at 21:7 Comment(0)
D
1

You can use libfaketime

https://github.com/wolfcw/libfaketime

LD_PRELOAD=src/libfaketime.so.1 FAKETIME="@2020-01-01 11:12:13"  phpunit

It will be as if you changed your system clock but only for that process, and it will work regardless of how low level your phpcode is

(Except if they use an external API call to get the time of course !)

Dropsonde answered 24/8, 2022 at 17:30 Comment(0)
R
1

You can also use uopz_set_return method. It allows to override return values for built-in functions as well as your custom functions.

For time() code will look like this:

 uopz_set_return('time', 1682406000);

In the end of your test you should remove mocked value:

 uopz_unset_return('time');
Rocco answered 3/5, 2023 at 21:27 Comment(0)
H
0

14 years later, just expanding on the accepted answer -

If you

  • don't mind using process isolation for the test function in PHPUnit
  • don't mind the dirtiness of using eval()

then you could do something like -

The thing you want to test:

namespace Your\Namespace\Here;

final class MyClass
{
    public function myFunction(): int
    {
        return time();
    }
}

The test class:

namespace Your\Test\Namespace\Here;

use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\RunInSeparateProcess;
use Your\Namespace\Here\MyRealClass;

final class MyClassTest extends TestCase
{
    #[RunInSeparateProcess]
    public function testMyFunction(): void
    {
        $myObject = new MyClass();
        eval('
            // Must be same as the class youre testing
            namespace Your\Namespace\Here; 

            function time(): int {
                return 123456;
            }
        ');

        $actual = $myObject->myFunction();

        self::assertSame(123456, $actual);
    }
}

Process isolation will reset the override in the namespace before the next test. This will allow you to use time() as normal, or mock it differently in the next test.

Heffron answered 24/3 at 23:5 Comment(1)
Isn't this the same approach that Fabian Schmengler already proposed thirteen years ago in https://mcmap.net/q/257850/-can-i-quot-mock-quot-time-in-phpunit?Extravagancy

© 2022 - 2024 — McMap. All rights reserved.