Solution 2: (ab)Using Doctrine


Towards the end of the previous video we had looked at one way to test dates and times with Behat. It wasn't a particularly elegant solution, but it allowed us to approach the problem from one perspective.

In truth this was the way I tested dates and times for a long while inside Behat.

On simple applications - very few endpoints, very few entities - this approach works well enough to allow you to move on quickly.

A better solution, in my opinion, is to mock the clock.

The problem with this approach is that by good God almighty this feels 'Enteprise-y'.

Mock Around The Clock Tonight

I put this series together solely to use that pun as a headline.

No, I kid. ho ho.

In researching a better solution to this problem I came across two important pieces of information:

  1. This GitHub ticket
  2. This post from Ross Tuck

GitHub Ticket

The issues list of any particular GitHub project (of sufficient size) is often a particularly useful source for learning, and problem solving.

In a project the size of Behat, the likelihood of me being the first developer to hit upon any particular problem is low.

That doesn't always mean someone will have opened a ticket and shared a solution, but it's a good place to start.

From reading this comment it was clear there was a potentially better solution to be found.

As we saw in this series, there can be problems in writing testable code if we don't own important concepts in our projects.

One of the most important, and often overlooked, is the concept of Time.

Typically if we want to work with a Date and Time, we new up an instance of \DateTime.

The problem in doing this is that time marches on unceasing.

This introduces an area of unhelpful change when trying to test this code.

User avant1 suggests instead that we abstract away the specifics of how we get the current time, and simply swap out implementations depending on environment.

In other words, when we are in Production, our clock should always be set to now.

In Development, however, we can set the clock to any time we please, and then use this known quantity as the basis for all our time-sensitive tests.

Seems good.

In Symfony this means we will create ourselves a new clock service, and then rather than rely on new \DateTime() directly, we will instead put the way the \DateTime is created behind a layer of indirection.

The problem now becomes how to access this clock from any place that needs the concept of date and time.

Previously we had this in our entity:

<?php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity()
 * @ORM\Table(name="widget")
 * @ORM\HasLifecycleCallbacks()
 */
class Widget
{
    // * snip *

    /**
     * Set created at
     *
     * @param \DateTimeImmutable $dateTime
     *
     * @ORM\PrePersist()
     *
     * @return $this
     */
    public function setCreatedAt(\DateTimeImmutable $dateTime = null)
    {
        if (null === $dateTime) {
            $dateTime = new \DateTimeImmutable('now');
        }

        $this->createdAt = $dateTime;
        $this->updatedAt = $dateTime;

        return $this;
    }

    /**
     * Set updated at
     *
     * @param \DateTimeImmutable $dateTime
     *
     * @ORM\PreUpdate()
     *
     * @return $this
     */
    public function setUpdatedAt(\DateTimeImmutable $dateTime = null)
    {
        if (null === $dateTime) {
            $dateTime = new \DateTimeImmutable('now');
        }

        $this->updatedAt = $dateTime;

        return $this;
    }

But this no longer works.

We can't just new up and instance of \DateTime, or \DateTimeImmutable here any longer, as we know we need to with via our new clock service.

Without this we have no control.

Aha!

I know, let's try using a Doctrine Listener.

Again, this is an approach we have used before.

Solution 2

By using a Doctrine Listener we can listen for events occurring 'system wide', and take some action accordingly.

What we could do here is to listen for prePersist and preUpdate events, and we could still set these values for ourselves automatically in the background.

The advantage of using a Doctrine Listener is that we can now inject other Symfony services.

Good news, everyone! Our clock can be setup as a Symfony service.

Let's set it up:

# app/config/config_prod.yml

imports:
    - { resource: config.yml }
    - { resource: services/clock_prod.yml }

and

# app/config/config_dev.yml

imports:
    - { resource: config.yml }
    - { resource: services/clock_prod.yml }

and

# app/config/config_acceptance.yml

imports:
    - { resource: config.yml }
    - { resource: services/clock_dev.yml }

This says in production AND development we want to use the service defined in services/clock_prod.yml.

This will be the clock that returns new \DateTimeImmutable('now').

It could return new \DateTime('now'). I would recommend using \DateTimeImmutable, however, as mutability is a huge cause of bugs. Mutability simply means can stuff change or not. Immutable things cannot change once set.

In acceptance mode we want to use the service defined in services/clock_acceptance.yml.

This will still be a clock. This will just be a clock that we can set.

We will have two different configurations of the same service, switched out depending on our environment.

You may be wondering why dev and prod are the same?

Why aren't we group acceptance and dev?

This is because we will very likely want to interact with our development setup in app_dev.php, and keep the constant flattening and repopulating of the acceptance environment's database from disturbing our dev work. This is my personal preference, anyway.

The service definitions give the game away:

# services/clock_prod.yml

services:

    crv.clock:
        class: 'AppBundle\Model\SystemClock'

vs

# services/clock_dev.yml

services:

    crv.clock:
        class: AppBundle\Model\TestClock
        arguments:
            - "@filesystem"
            - "%kernel.project_dir%"

Again, referring to avant1's example implementation what we could do is write the current time to a file, and read it if set, or return the current system time if not.

We're going to do approximately the same, only with two classes, not one.

Also, we will simplify the language based on Ross Tuck's suggestions.

Ok, so if we're going to have two different implementations of the same thing, the thing you are hopefully thinking is:

interface

Our Clock interface is going to be extremely simple:

<?php

namespace AppBundle\Model;

interface Clock
{
    public function now() : \DateTimeImmutable;
}

The production implementation simply returns a new instance of \DateTimeImmutable:

<?php

namespace AppBundle\Model;

class SystemClock implements Clock
{
    public function now() : \DateTimeImmutable
    {
        return new \DateTimeImmutable('now');
    }
}

The test clock is a lot more interesting.

<?php

namespace AppBundle\Model;

use Symfony\Component\Filesystem\Filesystem;

class TestClock implements Clock
{
    /**
     * @var Filesystem
     */
    private $filesystem;

    /**
     * @var string
     */
    private $dir;
    /**
     * @var string
     */
    private $timefile;

    public function __construct(Filesystem $filesystem)
    {
        $this->filesystem = $filesystem;

        $this->dir = sys_get_temp_dir() . '/Dummy/time/';
        $this->timefile = $this->dir . './timefile';
    }

    public function setTime(string $time)
    {
        $this->filesystem->mkdir($this->dir);

        if ($this->filesystem->exists($this->timefile)) {
            $this->filesystem->remove([$this->timefile]);
        }

        $isoTime = (new \DateTimeImmutable($time))
            ->format('c')
        ;

        $this->filesystem->dumpFile(
            $this->timefile,
            $isoTime
        );
    }

    public function now() : \DateTimeImmutable
    {
        if (false === $this->filesystem->exists($this->timefile)) {
            throw new \RuntimeException('Must set timefile before doing this.');
        }

        return new \DateTimeImmutable(
            file_get_contents($this->timefile)
        );
    }
}

Firstly I'm not sure AppBundle\Model is the right namespace for this code but for the moment that's not a major issue to me.

Because TestClockimplements Clock, we must have anow()method which must return aninstanceof \DateTimeImmutable`.

Our implementation of now() says this:

  • throw if we try to call now() before we have called setTime.
  • Otherwise get whatever time is set, and return it

I've added the guard clause to the now() function to make my life a bit easier when configuring (or mis-configuring) my TestClock.

The setTime() method is more involved, but is essentially what avant1 suggests:

We use Symfony's @filesystem service (a pre-configured instance of Symfony's Filesystem Component):

  • Create a temporary directory
  • Remove any leftover files that are like the one in which we will store our mocked time
  • new up a \DateTimeImmutable given whatever mocked time we need
  • Write (dumpFile) that value to the timefile

Making It Work With Behat

What we need to do now then is to get Behat to play ball.

We're going to need to set the time as part of our Background setup:

Feature: Manage Widget data via the RESTful API

  Background:
    Given the system time at the start of this test is "1 January 2020 00:00:00"
    And there are Widgets with the following details:
      | id | name     | created_at | updated_at |
      | 1  | widget A | -4 days    | -5 minutes |
      | 2  | widget B | -1 day     | -1 day     |
      | 3  | widget C | -6 months  | -3 weeks   |
    And I set header "Content-Type" with value "application/json"

  Scenario: Can get a single Widget
    When I send a "GET" request to "/widget/1"
    Then the response code should be 200
     And the response should contain json:
      """
      {
          "id": 1,
          "title": "widget A",
          "created_at": "2019-12-25T00:00:00+0000",
          "updated_at": "2019-12-31T23:55:00+0000"
      }
      """

We'd need a way for Behat to know about this new step:

<?php

// src/AppBundle/Features/Context/TimeContext.php

namespace AppBundle\Features\Context;

use AppBundle\Model\TestClock;
use Behat\Behat\Context\Context;

class TimeContext implements Context
{
    /**
     * @var TestClock
     */
    private $clock;

    /**
     * Initializes context.
     *
     * @param TestClock $clock
     */
    public function __construct(
        TestClock $clock
    )
    {
        $this->clock = $clock;
    }

    /**
     * @Given the system time at the start of this test is :strDate
     */
    public function theSystemTimeAtTheStartOfThisTestIs($strDate)
    {
        $this->clock->setTime(
            $strDate
        );
    }

}

and in behat.yml:

default:
  suites:
    default:
      type: symfony_bundle
      bundle: AppBundle
      contexts:
        - AppBundle\Features\Context\TimeContext:
            clock: "@crv.clock"

With our date and time issues under control, we are able to check for created_at and updated_at without fudging our tests.

This has improved our documentation. Also, we can get rid of the extra code we added in Behat to check for approximations of dates.

Writing The Listener

It's not elegant.

We could check if the given entity uses the Timestampable trait, but honestly this is equivalent in my eyes:

<?php

// src/AppBundle/Entity/Listener/TimestampListener.php

namespace AppBundle\Entity\Listener;

use AppBundle\Model\Clock;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;

class TimestampListener
{
    /**
     * @var Clock
     */
    private $clock;

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

    public function prePersist(LifeCycleEventArgs $args)
    {
        $entity = $args->getEntity();

        if (method_exists($entity, 'setCreatedAt')) {
            $entity->setCreatedAt(
                $this->clock->now()
            );
        }
    }

    public function preUpdate(PreUpdateEventArgs $args)
    {
        $entity = $args->getEntity();

        if (method_exists($entity, 'setUpdatedAt')) {
            $entity->setUpdatedAt(
                $this->clock->now()
            );
        }
    }
}

If the entity has a method called setCreatedAt then call it. Likewise for setUpdatedAt.

We call it with whatever the clock->now() call returns.

Depending on our environment this will either be the real, or mocked time.

We will need a service definition:

# app/config/services/event_listener.yml

services:

    crv.entity.timestamp:
        class: AppBundle\Entity\Listener\TimestampListener
        arguments:
            - "@crv.clock"
        tags:
            - { name: doctrine.event_listener, event: prePersist }
            - { name: doctrine.event_listener, event: preUpdate }

We can now remove the prePersist and preUpdate annotations from the setCreatedAt and setUpdatedAt methods on our Trait.

This works.

But I don't like it.

In the next video we will look my third alternative, and preferred solution.

Code For This Course

Get the code for this course.

Episodes

# Title Duration
1 Solution 1: Approximation 11:04
2 Solution 2: (ab)Using Doctrine 15:44
3 Solution 3: Mock The Clock 10:02