Unit testing is a development topic that I'm fairly passionate about. It is, unfortunately, a bit tricky to understand when you're just getting started. Given that some operations in PHP can introduce uncertainty in your code, there are situations where testing is just plain hard and developers easily get discouraged.
One of the trickier operations is time().
A PHP Use Case
One of my newer PHP extensions is a library for generating and validating time-based one-time-password (TOTP) codes.
The core of the extension is a function to generate a TOTP code given a known secret key. It leverages the system clock (time()) as a counter and will generate the same code for the same time step every time. The following is drastically simplified from the extension's actual code:
function calc_totp($key, $digits = 6, $hash = 'sha1', $time_step = 30)
{
$secret = Encoding::base32DecodeUpper((string)$key);
// Pad the secret value if necessary ...
$time_step = intval($time_step);
$step_count = floor(time() / $time_step);
$timestamp = pack('J', $step_count);
$hash = hash_hmac($hash, $timestamp, $secret, true);
// Encode the hash ...
return str_pad($code, $digits, '0', STR_PAD_LEFT);
}
Testing this code can be a bit tricky. On the one hand, it's fairly straight-forward to generate a code then immediately validate it with the same library. Codes are valid for up to ~4 minutes, so you're not dealing with any race conditions there.
However, there's not really a clear way to generate deterministic codes as we're bound to a system-level call to time().
The TOTP specification includes a set of reference values for testing custom implementations – given a specific secret at a specific time, any valid TOTP implementation should produce a specific result. In order to add these reference values to our test suite, we need a way to substitute a deterministic value for the system time.
Mocking Time
Luckily, the project itself leverages PHP namespaces for encapsulation. This helps keep my code separate from everyone else's code, but also comes with a great ancillary benefit for testing.
When a function is called without a namespace modifier, PHP will by default look within the current namespace for an implementation. If none is found, the interpreter will instead look in the global namespace.
This means we can redefine time() within our test namespace to override the system implementation!
assertEquals(1514996409, time());
}
}
By default, invoking time() in a test will return the result of a system call as the static timestamp isn't set. However, if we need to override that timestamp to set a specific value, we just set $now to our value and time() will return it on every invocation.
This is a simple trick to make time-based functionality in PHP easily testable.