Solution 1: Approximation


Behat is a really useful and powerful tool. Not only does it allow us to write automated acceptance tests, but with a bit of careful thought, can also serve as the "living documentation" to our projects as well.

One of the trickiest parts of testing, regardless of whether with Behat or other tools, is when we need to work with Dates and Times. This can be with PHP's \DateTime, or more recently (and dare I say, more cool-ly :)) \DateTimeImmutable.

The issue with dates and times is that they are - usually - relative to whenever we run our tests.

Take as an example a made up entity I just invented:

<?php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity()
 * @ORM\Table(name="widget")
 */
class Widget
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @ORM\column(type="string", name="name")
     */
    protected $name;

    // getters and setters etc
}

So far, so basic.

Typically on the more interesting parts of our projects we want to track when entities have been createdAt, and updatedAt.

One solution to this problem is to simply add two new class properties:

    /**
     * @var \DateTimeImmutable $createdAt Created at
     *
     * @ORM\Column(type="datetime_immutable", name="created_at")
     */
    protected $createdAt;

    /**
     * @var \DateTimeImmutable $updatedAt Updated at
     *
     * @ORM\Column(type="datetime_immutable", name="updated_at")
     */
    protected $updatedAt;

I already have some a big issue with this implementation.

The problem here is that updatedAt is pretty much useless.

Sure, we know that this entity was updated on a particular date, and at a particular time, but so what?

Who updated it?

What did they change, and why?

Without quite a lot of extra effort we have no idea about the intent of the change, but yet adding in these properties feels like we've achieved something.

This is really just the tip of the iceberg regarding this particular problem though. I must admit to being both guilty of doing this, and helping persist the problem.

Automate The Boring Stuff

Yes, not only is it seemingly common practice to add these properties to our entities, we tend to reach for Doctrine's Lifecycle Callbacks in an attempt to automatically update these values whenever creating or updating entities in our projects.

All of this seems like useful and helpful stuff though, right?

We want to capture information about when things are changing in our applications, and because creating and updating entities is a fairly common thing to do, we don't want to manually manage the process of capturing this "change".

Ok, so let's add in these Lifecycle Callbacks:

<?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;
    }

Sadly though, this won't work quite as expected.

When Doctrine tries to call the setCreatedAt, and setUpdatedAt methods as a result of us using the annotations - @ORM\PrePersist(), and @ORM\PreUpdate() - it will attempt to call method setCreatedAt with LifecycleEventArgs, and setUpdatedAt with a PreUpdateEventArgs object.

Our implementation expects an instance of \DateTimeImmutable.

We can hack around this:

<?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
     *
     * @return $this
     */
    public function setCreatedAt(\DateTimeImmutable $dateTime = null)
    {
        if (null === $dateTime) {
            $dateTime = new \DateTimeImmutable('now');
        }

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

        return $this;
    }

    /**
     * Set created at via Doctrine PrePersist
     *
     * @ORM\PrePersist()
     *
     * @return $this
     */
    public function setCreatedAtViaPrePersist()
    {
        return $this->setCreatedAt();
    }

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

        $this->updatedAt = $dateTime;

        return $this;
    }

    /**
     * Set updated at via Doctrine PreUpdate
     *
     * @ORM\PreUpdate()
     *
     * @return $this
     */
    public function setUpdatedAtViaPrePersist()
    {
        return $this->setUpdatedAt();
    }

This works because even though our two new methods will be called with the LifecycleEventArgs and PreUpdateEventArgs respectively, the fact that these two methods don't use any function arguments means this little fact is completely ignored :/

It's a hacky, nasty solution that leaves me feeling queasy.

However, at least now whenever we persist a new entity, or flush changes on an existing entity, our createdAt and updatedAt class properties are automagically updated for us.

You may be wondering why having these properties set automatically this is a bad thing.

Well, let's make this a little more real world.

Let's imagine that we have a JSON API where our Widget can be Created, Updated, and Read through a typical REST-like interface.

Widget JSON API

We're going to start by writing a Behat feature for our Widget endpoint.

As I said at the start of this write up, one of my favourite parts of Behat is in its ability to serve as Living Documentation. By this I mean that our Behat feature files explain exactly how interaction with our JSON API should work.

We can refer back to this documentation to figure out how our API behaves. This is great not just for whilst we are actively developing, but also for future reference when we have worked on 20 other projects since the last time we looked at this code base, and for quickly on-boarding new devs to the project - both front-end, and back.

Here's our example GET /widget/1 test setup:

Feature: Manage Widget data via the RESTful API

  Background:
    Given there are Widgets with the following details:
      | id | name     |
      | 1  | widget A |
      | 2  | widget B |
      | 3  | widget C |
    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"
      }
      """

Ok, so there's a bunch of stuff going on behind the scenes here that sets all of this up, but the gist of this implementation is nothing we haven't seen before.

Note though, the lack of any Date or Time information captured in our tests.

Behat actually won't mind this.

So long as our API response contains some JSON with the keys of id and title, we should be good. Why this is poor, in my opinion, is that our "Living Documentation" is inaccurate.

Developers - whether that is us, or a colleague - won't immediately know from looking at this that we also return the created_at and updated_at information.

In reality a request to /widget/1 may look more like this:

{
  "id": 1,
  "title": "widget A",
  "some": "other data",
  "blah": "yadda yadda",
  "created_at": "2016-12-25T00:00:00+0000",
  "updated_at": "2016-12-31T23:55:00+0000"
}

Essentially so long as the fields we care about are there, we don't explicitly check that the returned response is an exact match to our expected JSON. Good for us (as it helps get to a passing test), but bad for the project overall.

As you might have guessed by now, writing a test that covers created_at and updated_at is problematic as the values will change with every test run.

Solution #1

It might be that the created_at and updated_at datetimes are important bits of data that we absolutely want to both test, and by extension, document.

We can start by updating our Behat feature to allow us to override the createdAt and updatedAt values.

Remember, as we have HasLifecycleCallbacks annotation on our Widget, right now these values are going to be set automatically for us whenever we create or update our entity.

Feature: Manage Widget data via the RESTful API

  Background:
    Given 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"
      }
      """

Now we're using a couple of new columns in our Background step to ensure we have set some known parameters as to when our entity should have been created, and updated.

We'd need to add some extra setup info here. One such way might be:

    /**
     * @Given there are Widgets with the following details:
     */
    public function thereAreWidgetsWithTheFollowingDetails(TableNode $widgets)
    {
        foreach ($widgets->getColumnsHash() as $key => $val) {

            $widget = (new Widget())
                ->setName($val['name'])
                ->setCreatedAt(
                    new \DateTimeImmutable($val['created_at'])
                )
                ->setUpdatedAt(
                    new \DateTimeImmutable($val['updated_at'])
                )
            ;

            $this->em->persist($widget);
            $this->em->flush();
        }
    }

Seems ok.

And when we run it through, sure enough each of our Widgets are instantiated and configured with the properties defined in our Given there are Widgets with the following details: step.

The problem is that created_at and updated_at are relative to the time that our tests ran.

We are smart developers though, so this is not a huge problem.

We just need to add in a suboptimal solution and head out for an early lunch.

Feature: Manage Widget data via the RESTful API

  Background:
    Given 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"
      }
      """
     And the "created_at" date should be approximately "-4 days"
     And the "updated_at" date should be approximately "-5 minutes"

Good enough, right?

Well, sort of.

Let's quickly look at what that new step definition is doing:

    /**
     * @Then the :selector date should be approximately :date
     * @throws \InvalidArgumentException
     * @throws \PHPUnit_Framework_AssertionFailedError
     */
    public function theDateShouldBeApproximately($selector, $date)
    {
        $responseBody = $this->getResponseBody();
        $dateTimeFromResponse = new \DateTime($responseBody[$selector]);
        $expectedDateTime = new \DateTime($date);

        Assertions::assertTrue(
            $expectedDateTime
                ->diff($dateTimeFromResponse)
                ->format('%s')
            < 5
        );
    }

It's pretty nasty to read, but here's what's happening:

$this->getResponseBody()

Is a call to this method:

    /**
     * @return array
     */
    protected function getResponseBody()
    {
        return json_decode($this->response->getBody(), true);
    }

If not clear, this gives us an associative array containing the outcome of converting the JSON response to a PHP array. In our case this will be an array with a key of name and a value of widget A, and so on, for each key/value pair in our API response.

Given that we now have a nice associative array to work from, we can get a little further.

$dateTimeFromResponse = new \DateTime($responseBody[$selector]);

$selector here is the first argument from our step definition:

And the "created_at" date should be approximately "-4 days"

Our $selector will therefore be the string created_at.

Our associative array has the key created_at. Therefore dateTimeFromResponse will contain the outcome of a new \DateTime with whatever value is returned on the response from our API.

The second argument in our step definition - the string -4 days from the above - would be available to us as the $date variable.

$expectedDateTime = new \DateTime($date); - therefore is a little simpler to understand.

Finally we make use of PHPUnit's Assertion library to check that the difference between the time from the response, and the expected time is less than 5 seconds. This should be loose enough for our satisfy our tests, but confined enough to be considered reasonably accurate.

This is good because now our tests include the dates and times portion of our Widget, but don't rigidly tie us to a specific date and time.

This is bad because we aren't capturing this information as part of our JSON response. Developers need to 'grok' this to understand that a created_at and updated_at property will be available. It's not the most difficult thing to read, but it's also not super straightforward.

You might be satisfied with this. In truth, I was satisfied with this for a while.

Where it falls apart is when we have nested relations.

Let's say a widget has some features, which themselves also have these created_at and updated_at properties:

Feature: Manage Widget data via the RESTful API

  Background:
    Given 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",
          "features": [
            {
                "id": 6,
                "name": "long and pointy",
                "created_at": "2015-01-01T00:00:00+0000",
                "updated_at": "2015-01-02T23:55:00+0000"
            },
            {
                "id": 9,
                "name": "smooth and shiny",
                "created_at": "2016-02-18T00:00:00+0000",
                "updated_at": "2016-02-19T11:16:00+0000"
            }
          ]
      }
      """
     And the "created_at" date should be approximately "-4 days"
     And the "updated_at" date should be approximately "-5 minutes"

This unfortunately won't work.

And you might be thinking: no problem, I will simply remove the created_at and updated_at properties from the nested Features. Alas, that won't work either.

Then how on Earth do we fix this?

One possible answer is that we mock the clock. We will get to that in the next video.

Code For This Course

Get the code for this course.

Code For This Video

Get the code for this video.

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