Andrew
Andrew

Reputation: 238707

PHPUnit: How to mock today's date without passing it as an argument?

I am testing a method on my class that does some date checking. The problem is that the method depends on today's date (which changes every day), which makes this difficult to test. How can I mock today's date so that my tests will still pass tomorrow?

Upvotes: 14

Views: 8357

Answers (5)

Mike Hedman
Mike Hedman

Reputation: 1441

If your interest is to not pass in the date to preserve the external interface, then a good way to do this is to use a "seam" to provide the date:

class MyClass {
  public function toBeTested() {
    $theDate = $this->getDate();
    ...
  }

  protected function getDate() {
    return date();
  }
}

In general use, this class just works normally. Then, in your unit testing, instead of testing MyClass, you extend MyClass with an inner class that overrides the getDate() function:

use PHPUnit\Framework\TestCase;

class MyTest extends TestCase {

  static $testDate;

  public function testToBeTested() {
    //set the date to be used
    MyTest::testDate = '1/2/2000';
    $classUnderTest = new MyClassWithDate();
    $this->assertEquals('expected', $classUnderTest->toBeTested());

  }
}

//just pass back the expected date
class MyClassWithDate extends MyClass {
  protected function getDate() {
    return MyTest::testDate;
  }
}

In this code, you test against your extension of the real class, but your extension overrides the seam function (getDate()), and returns back the date that you want to use for this particular test.

Again, sorry if there are some egregious syntax errors, this was written freehand.

Upvotes: 6

jhvaras
jhvaras

Reputation: 2124

I couldn't rename twice as David says, so I got it like:

function mockDate()
{
    runkit_function_rename('date', 'test_date_override');
    runkit_function_add('date','$format=NULL,$timestamp=NULL,$locale=NULL', 'return DATEMOCK;');
}

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

Upvotes: 0

KLE
KLE

Reputation: 24159

I know you don't want to pass it in as an argument. But maybe you can rethink this ...

When being passed as a parameter from the outside, the date is not an insignificant technical detail, but a significant functional rule. Don't you need any of the following?

  • Although the current date can be the regular use case, the rule might be applicable to another date. Then your code would be more general, and work in a later use case with no modification. That happens to me regularly ...
  • Several codes could use the current date in an algorithm. Because the computer speed is not infinite, several would get a different instant ... Is that logical functionally? Or would using the same instant (for example, the instant your user pressed the "Fire" button) be more accurate? Think how you might request these times in your database later on, if they are all different in your database, even if they represent the same instant for your user!

Upvotes: 3

David Harkness
David Harkness

Reputation: 36532

While Jon's answer is the "right way," another option is to use the runkit extension to temporarily replace the date() and/or time() functions with ones that return a fixed value for the test.

  1. Make sure to set runkit.internal_override in php.ini so you can rename built-in functions.
  2. Rename the original function using runkit_function_rename.
  3. Rename your mock function with the original's name.
  4. Test.
  5. Rename your mock back.
  6. Rename the original back.

Here's some completely untested code to help with this:

function mock_function($original, $mock) {
    runkit_function_rename($original, $original . '_original');
    runkit_function_rename($mock, $original);
}

function unmock_function($original, $mock) {
    runkit_function_rename($original, $mock);
    runkit_function_rename($original . '_original', $original);
}

You should use these from within the setUp() and tearDown() methods to make sure you don't interfere with other tests that follow.

Upvotes: 4

Jon Skeet
Jon Skeet

Reputation: 1500385

I know nothing about PHP, but in both Java and C# I would pass in a "clock" of some description - not today's date itself, but an object which you can ask for the current date/time. Then in unit tests you can pass in an object which can give any date you want - including one that's hard-coded into the tests.

Does that work in PHP too?

Upvotes: 9

Related Questions