5

I'm new to php unit testing. How do I mock the date in the function below. Currently it is getting the current date. But I want to change the date in the mock to the first day of a month.

function changeStartEndDate() {

    if (date('j', strtotime("now")) === '1') {

        $this->startDate = date("Y-n-j", strtotime("first day of previous month"));

        $this->endDate = date("Y-n-j", strtotime("last day of previous month")) . ')';
    } else {

        $this->startDate = date("Y-n-j", strtotime(date("Y-m-01")));
        $this->endDate = date("Y-n-j", strtotime("yesterday"));
    }
}

I've tried doing this but its not working.

public function testServicesChangeStartEndDate() {
    $mock = $this->getMockBuilder('CoreFunctions')
        ->setMethods(array('changeStartEndDate'))
        ->getMock();

    $mock->method('changeStartEndDate')
        ->with(date("Y-n-j", strtotime(date("Y-m-01"))));

    $this->assertSame(
        '1',
        $this->core->changeStartEndDate()
    );

}
k0pernikus
  • 60,309
  • 67
  • 216
  • 347
SimpleProgrammer
  • 158
  • 3
  • 13
  • Potential duplicate: https://stackoverflow.com/questions/2371854/can-i-mock-time-in-phpunit – k0pernikus Dec 12 '19 at 12:11
  • Are you trying to test the CoreFunctions or are you using it as a mock for a different test case? – k0pernikus Dec 12 '19 at 12:37
  • I am trying to test CoreFunctions – SimpleProgrammer Dec 12 '19 at 14:52
  • In that case: You should never have the need to mock anything that is part of your CoreFunctions. I consider it a code smell if you create a mock for your unit under test. Only if you test another service that uses your corefunctions does it make sense to use a mocked version of your corefunctions. I dare say that for this test case, you don't need to create any mock. You are trying to use a mock to solve the issues you are having due to the side-effects due to the current time. If you change your code to be without side-effects, then you don't need to mock anything. – k0pernikus Dec 12 '19 at 14:57
  • If you do run into something that you want to mock inside your `CoreFunctions`, it's a strong indicator that your functions / classes / methods are doing too much and that you should move it into an extra service. When doing unit tests always wonder: What is the actual unit I want to test? It should be well-defined, at best a pure function. The name `CoreFunctions` implies a utils library that most likely is already overblown in scope. – k0pernikus Dec 12 '19 at 15:02

2 Answers2

9

Unit testing works best by avoiding side effects. Both date and strtotime are depending on an external state defined on your host system, namely the current time.

One way to deal with that is to make current time an injectable property allowing you to "freeze" it or to set it to a specific value.

If you look at at the definition to strtotime it allows setting current time:

strtotime ( string $time [, int $now = time() ] ) : int

Same with date:

date ( string $format [, int $timestamp = time() ] ) : string

So always inject that value from your function to decouple the results of your code from your host's state.

function changeStartEndDate($now) {

    if (date('j', strtotime("now", $now), $now) === '1') {
        ...
        $this->startDate = date("Y-n-j", strtotime(date("Y-m-01", $now), $now));
        $this->endDate = date("Y-n-j", strtotime("yesterday", $now), $now);
    }

Is your function part of a class? I would then make the $now part of the constructor, and have it default to time(). In your test cases, you can always inject a fixed number, and it will always return the same output.

class MyClassDealingWithTime {
    private $now;

    public function __construct($now = time()) {
        $this->now = $now;
    }


    private customDate($format) {
        return date($format, $this->now);
    }

    private customStringToTime($timeSring) {
        return strtotime($timeStrimg, $this->now);
    }
}

In your test cases you then set $now to the value you need, e.g. via

$firstDayOfAMonth = (new DateTime('2017-06-01'))->getTimestamp();
$testInstance = new MyClassDealingWithTime(firstDayOfAMonth);

$actual = $testInstance->publicMethodYouWantTotest();

... 
k0pernikus
  • 60,309
  • 67
  • 216
  • 347
1

Disclaimer: I wrote the library mentioned on this answer.

I'm adding an answer to provide an alternative way that works with zero modifications to your code, and no need to inject the current time.

If you can afford to install the php uopz extension, then you can use https://github.com/slope-it/clock-mock.

You can then use ClockMock::freeze and ClockMock::reset to "move" the internal php clock to a specific point in time during your tests.

SuperDuper
  • 19
  • 8
Andrea Sprega
  • 2,221
  • 2
  • 29
  • 35