in Test Driven Development

How to handle time in your tests?

Handling time in Unit Tests

Often we face scenario’s where a process we are modelling is depending on time. Take for example an expiring coupon. A coupon is no longer valid when it has expired. In PHP we could implement this as follows:

class Coupon
{
    /**
     * @var \DateTimeImmutable
     */
    private $expiryDate;

    public function __construct(\DateTimeImmutable $expiryDate)
    {
        $this->expiryDate = $expiryDate;
    }

    public function isValid(): bool
    {
        $diffInSeconds = $this->expiryDate->getTimestamp() - time();
        return $diffInSeconds > 0;
    }
}

class CouponTest extends TestCase
{
    public function testANotExpiredCouponIsValid(): void
    {
        $coupon = new Coupon(new \DateTimeImmutable('2018-10-28T07:14:49+02:00'));
        self::assertTrue($coupon->isValid());
    }

    public function testAnExpiredCouponIsNotValid(): void
    {
        $coupon = new Coupon(new \DateTimeImmutable('2017-10-28T07:14:49+02:00'));
        self::assertFalse($coupon->isValid());
    }
}

The problem

When we run the test for a valid coupon someday after 2018-10-28 (the date it will expire), it will actually fail because the coupon has actually expired. That’s not what we want because we need to test coupons with an expiry date in the future. So we need a way to fix that.

One way to resolve this issue would be to just change our test and pass in a date that’s 500 years in the future. It will fail again in 500 years but it will not be our problem anymore.

A better way to resolve this is to actually pass the current time as an argument to the isValid() method. Look at the new implementation:

class Coupon
{
    /**
     * @var \DateTimeImmutable
     */
    private $expiryDate;

    public function __construct(\DateTimeImmutable $expiryDate)
    {
        $this->expiryDate = $expiryDate;
    }

    public function isValid(\DateTimeImmutable $now): bool
    {
        $diffInSeconds = $this->expiryDate->getTimestamp() - $now->getTimestamp();
        return $diffInSeconds > 0;
    }
}

class CouponTest extends TestCase
{
    public function testANotExpiredCouponIsValid(): void
    {
        $coupon = new Coupon(new \DateTimeImmutable('2018-10-28T07:14:49+02:00'));
        self::assertTrue($coupon->isValid(new \DateTimeImmutable('2018-10-01T00:00:00+02:00')));
    }

    public function testAnExpiredCouponIsNotValid(): void
    {
        $coupon = new Coupon(new \DateTimeImmutable('2017-10-28T07:14:49+02:00'));
        self::assertFalse($coupon->isValid(new \DateTimeImmutable('2018-10-01T00:00:00+02:00')));
    }
}

That’s better. Now we have frozen the current time and are no longer depend on the real system type. Our tests will always pass.

Write a Comment

Comment