Improve your local web app development with Docker

Have you ever had a situation where there was a bug in some deployed application, but the simple act of running that application locally made the bug impossible to find? It could've been that there was no local infrastructure (e.g. databases) to back the running services. Or port-configuration was hard-coded into the application (so it collided with other ports on your host machine; people love to hard-code to port 8000 , 80 or 8080 ). Or, scarier still, the application was never actually run locally because it was deemed too difficult in the first place (in which case, you wonder how the deployed application worked at all)? In an age where we have Docker to effectively ship the containerized machines that our applications run on, there really isn't an excuse for making local development a more consistent experience. If there is greater confidence in running code locally, it becomes quicker to spot and fix bugs. It becomes quicker to onboard new developers and make them pr...

Code Smells: Static References to Time

So you need to do some time-specific calculations in your application. It’s related to the current time, so you do the usual thing, and new up a DateTime object:

$now = new DateTime();

Seems innocuous, right? But you quickly run into some pretty nasty problems:

  • Your code is now very difficult to test reliably. To make any assertions on the behaviour that aren't time-flakey (i.e. they work today but not in a few months time), you’ll now need to do something with the internal system clock while the test is running. Perhaps you can get away with running your tests in a container whose clock is fixed to the desired time, but this is a considerable amount of work to get around the brittleness of the code itself (we will see below how to achieve this same outcome without the need for such machinery).
  • If something is "off" with the internal system clock (e.g. the timezone isn't the one you'd expected) and you need to make a change to your application code, you need to make this change in all areas of the code that directly instantiate the DateTime object. That's potentially a lot of room for error: a bug just waiting to happen.

Your application has time-dependent behaviour, and so it is worth structuring it to make this explicit and easy to manage. The solution? A clock!

interface Clock
{
    public function now(): DateTimeInterface;
}

Instead of directly instantiating a DateTime object, you use a factory which returns DateTime instances. In this case, our “factory” has a common-sense name: it returns the current time, so it has the behaviour of a Clock. Pass this as an argument to the method or as a constructor for the service object concerned, and voila:

$this->clock->now();

This cleanly solves both of the issues we saw earlier.

On the testing front, we have two implementations of the clock. The production implementation is straight-forward: just new up a DateTime object:

final class LocalClock implements Clock
{
    public function now(): DateTimeInterface
    {
        return new DateTimeImmutable();
    }
}

For tests, we use an implementation that is most useful for the test criteria. We can implement a clock that always returns the same time:

final class StaticClock implements Clock
{
    private DateTimeInterface $time;

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

    public function now(): DateTimeInterface
    {
        return $this->time;
    }
}

This is great when we want to test behaviour where the clock is only expected to be called once. For example, suppose I have a greeter which greets someone a time-sensitive message. Prior to 12pm it greets them "Good morning"; prior to 6pm it greets them "Good afternoon", and after then it greets them "Good evening".

final class Greeter
{
    private Clock $clock;

    public function __construct(Clock $clock)
    {
        $this->clock = $clock;
    }

    public function greet(string $name): string
    {
        // get the current hour
        $hour = (int)$this->clock->now()->format('H');

        if ($hour < 12) {
            return 'Good morning, ' . $name;
        } elseif ($hour < 18) {
            return 'Good afternoon, ' . $name;
        } else {
            return 'Good evening, ' . $name;
        }
    }    
}

Testing this is trivial. For each scenario, we create a clock whose time is fixed to the desired one, and assert that the response is the intended one:

$morningGreeter   = new Greeter(new StaticClock(new DateTimeImmutable('2020-01-01 06:00:00')));
$afternoonGreeter = new Greeter(new StaticClock(new DateTimeImmutable('2020-01-01 12:00:00')));
$eveningGreeter   = new Greeter(new StaticClock(new DateTimeImmutable('2020-01-01 18:00:00')));

assert('Good morning, stranger' === $morningGreeter->greet('stranger'));
assert('Good afternoon, stranger' === $afternoonGreeter->greet('stranger'));
assert('Good evening, stranger' === $eveningGreeter->greet('stranger'));

In a situation where we call the clock multiple times, we may want an implementation that returns a different DateTime on each now() invocation. For example, suppose we were timing a function call:

function secondsTimer(Clock $clock, function $callback): int
{
    $started = $clock->now();
    $callback();

    // in practice you would use greater precision than seconds
    // this is just to illustrate the concept
    return $started->diff($clock->now())->s;
}

This can be tested with a clock that holds a stack of different times, pulling the first element off the stack with each call:

final class StackClock implements Clock
{
    private array $times;

    public function __construct(DateTimeInterface ...$times)
    {
        $this->times = $times;
    }

    public function now(): DateTimeInterface
    {
        return array_shift($this->times);
    }
}

Just pass in the desired times:

$clock = new StackClock(
    new DateTimeImmutable('2020-01-01 00:00:00'),
    new DateTimeImmutable('2020-01-01 00:00:03'), // <- 3 second difference
);
$callback = function () {
    // do nothing  
};

assert(3 === secondsTimer($clock, $callback));

...and you're done!

Without using a Clock your only real option for testability would be to use a function that sleeps for the desired amount of time and hope that nothing untoward happens when running the test:

$callback = function() {
    sleep(1);  
};
assert(1 === secondsTimer($callback));

With respect to changing our clock on the off chance of a timezone error, we can implement a clock that always returns times in a fixed timezone:

final class FixedTimeZoneClock implements Clock
{
    private DateTimeZone $zone;

    public function __construct(DateTimeZone $zone)
    {
        $this->zone = $zone;
    }

    public function now(): DateTimeInterface
    {
        return new DateTimeImmutable('now', $this->zone);
    }
}

The change is localised (no pun intended) to the clock, and the implementation details of how the time is found are hidden from everywhere else in the code. We have a strategy that maximizes the testability of the code under time-dependent behaviour without sacrificing any functionality, and it wasn't difficult to implement, either!

As Kevlin Henney succinctly put it:

Time is a resource, just like a file.

The very same strategy for abstracting away the details of file IO from the rest of our code are applicable to abstracting "time IO".

Comments

Popular posts from this blog

Improve your local web app development with Docker

Shifting from OOP Web Components to FP: A brief look at Elm

Using the POST method for HTTP search queries