PHP: “Mocking” built-in functions like time() in Unit Tests

A common problem in Unit Testing in PHP is testing something that depends on the current time. For a determined test it should be possible to set the time in your test script without really changing the system settings. In this article I’ll describe how it is usually done with OOP and then come to an alternative solution with much less code that makes use of the new features in PHP 5.3.

The usual approach would be a wrapper class like this:

class Calendar
{
    public function time()
    {
        return time();
    }
    public function date($format, $time = null)
    {
        return date($format, $time ?: $this->time());
    }
    // ...
}

Now any class that uses date/time functions has to be modified to use the Calendar class via Dependency Injection:

class SomeClass
{
    /**
     * @var Calendar
     */
    private $calendar;

    public function __construct(Calendar $calendar = null)
    {
        $this->calendar = $calendar ?: new Calendar;
    }
    public function oneHourAgo()
    {
        return $this->calendar->date('H:i:s', $this->calendar->time() - 3600);
    }
}

Then you mock the Calendar class in your tests and pass it to the test subject. I won’t go into further details because you probaly know the concept of mocking and how to do this in your favourite unit testing framework. After all this article is not about mocking classes, because I have:

A simpler solution with namespaces

If you are using PHP 5.3 namespaces you are lucky because you won’t need all this overhead and probably no changes in your classes at all. The trick is to override built-in functions in your current namespace. Consider this namespaced version of the class from above:

namespace My\Namespace;

class SomeClass
{
    public function oneHourAgo()
    {
        return date('H:i:s', time() - 3600);
    }
}

As you can see, no overhead, just a straightforward call to date() and time(). To test this with specific times we implement a test case as follows (Example in PHPUnit but works as well with other frameworks):

namespace My\Namespace;

require_once 'PHPUnit\Framework\TestCase.php';

/**
 * Override time() in current namespace for testing
 *
 * @return int
 */
function time()
{
	return SomeClassTest::$now ?: \time();
}

class SomeClassTest extends \PHPUnit_Framework_TestCase
{
	/**
	 * @var int $now Timestamp that will be returned by time()
	 */
	public static $now;

	/**
	 * @var SomeClass $someClass Test subject
	 */
	private $someClass;

	/**
	 * Create test subject before test
	 */
	protected function setUp()
	{
		parent::setUp();
		$this->someClass = new SomeClass;
	}
	/**
	 * Reset custom time after test
	 */
	protected function tearDown()
	{
		self::$now = null;
	}

	/*
	 * Test cases
	 */
	public function testOneHourAgoFromNoon()
	{
		self::$now = strtotime('12:00');
		$this->assertEquals('11:00', $this->someClass->oneHourAgo());
	}
	public function testOneHourAgoFromMidnight()
	{
		self::$now = strtotime('0:00');
		$this->assertEquals('23:00', $this->someClass->oneHourAgo());
	}
}

The crucial point here is that we implement a new function named exaclty like a built-in function. You cannot replace functions but since this is defined in the namespace \My\Namespace it does not replace anything. In fact it is a new function with the fully qualified name \My\Namespace\time()

The test subject now calls time() as unqualified name so PHP looks for the function in the current namespace at first. That is \My\Namespace\time() in our example. I recommend the section about name resolution rules in the manual for further reading.
Important Implication: It does not work if you use the global functions with fully qualified names (i.E. \time()) in your test subjects!

You can implement this function however you like, I decided to make the return value configurable within the test case via a static property that gets resetted after each test and if it is not set the real time is used.

I hope this solution helps, it may feel hackish but for me it made testing a lot easier!

Magento: Make New Customer Attributes Visible in Backend Forms

If you add a new attribute to the customer entity in a a setup script, using addAttribute(), gets a surprise starting from Magento 1.5. The setup runs and database entries in customer_eav_attribute are created correctly but in the backend there is no new form element.

Which works well with other entities like categories and until Magento 1.4 also with customers, needs an additional setup step now, to add the attribute explicitly to the admin form (and/or other forms, like the registration form).

There is a new table for it, customer_form_attribute, which defines for each attribute, in which forms it can be used:

mysql> select * from customer_form_attribute;
+----------------------------+--------------+
| form_code                  | attribute_id |
+----------------------------+--------------+
| adminhtml_customer         |            1 |
| adminhtml_customer         |            3 |
| adminhtml_customer         |            4 |
| checkout_register          |            4 |
| customer_account_create    |            4 |
| customer_account_edit      |            4 |
| adminhtml_customer         |            5 |
| checkout_register          |            5 |
| customer_account_create    |            5 |
| customer_account_edit      |            5 |
| adminhtml_customer         |            6 |
| checkout_register          |            6 |
| customer_account_create    |            6 |
| customer_account_edit      |            6 |
| adminhtml_customer         |            7 |
| checkout_register          |            7 |
| customer_account_create    |            7 |
| customer_account_edit      |            7 |
[...]
+----------------------------+--------------+
88 rows in set (0.00 sec)

So much to for a better understanding, of course you don’t need to (and should not!) modify the database manually, it is sufficient to add the following lines after the addAttribute() call in your setup script:

    Mage::getSingleton('eav/config')
        ->getAttribute('customer', $attributeCode)
        ->setData('used_in_forms', array('adminhtml_customer'))
        ->save();

with $attributeCode being the unique code of the new attribute and 'adminhtml_customer' the code for the customer management form in the backend. To keep setup scripts more clear if you add multiple attributes, it helps to extend the entity setup class, like this:

class MyNamespace_ExtendableCustomer_Model_Entity_Setup
    extends Mage_Customer_Model_Entity_Setup
{
    public function addAttributeToForm($attributeCode, $formCode)
    {
    	Mage::getSingleton('eav/config')
            ->getAttribute('customer', $attributeCode)
            ->setData('used_in_forms', (array) $formCode)
            ->save();
    }
}

Now when you use this class for your setup scripts that add customer attributes, it’s as simple as:

$this->addAttribute('customer', 'new_attribute', array( /* ... */);
$this->addAttributeToForm('new_attribute', 'adminhtml_customer');

Of course you can add the attribute to other forms in the same way, for the codes see the list above.

Thank you to the Magento forums for the crucial hint.

Update

Pay attention, on save the field eav_entity_attribute.sort_order gets set, regardless being specified before!

See my comment on StackOverflow.

Furthermore sort_order only applies if user_defined => 0 was specified in the attribute setup because Magento sorts by user_defined first and then by sort_order.