User Feature - Part 1


In this video we are going to get started writing the Behat feature for managing our FOSUserBundle User data via the RESTful API.

I want to say before proceeding that you may disagree with the way I use Behat. That's fine. I am not the world's foremost expert on Behat / BDD, and so, am always open to improving - feel free to leave a comment.

There are differences between using BDD for a large scale enterprise project with multiple stakeholders, and using BDD for personal projects such as this. Effectively I am talking to myself in these features, writing a user manual for my future self to understand what I was thinking at the time of implementing.

The language I use in my features is more technical than you may typically find in a 'corporate' Behat feature.

And most importantly, don't let the fear of writing imperfect features be a reason to avoid using Behat. It takes practice, and that practice never stops. Hopefully with each feature you get a little better at understanding how these files work best for you, and your team, and organisation.

Feature Overview

For the purposes of this demonstration, we are only going to allow the logged in User to update their own data. This means they cannot create new Users, nor delete their own or other existing User data. We will stop this from happening using Symfony Security Voters.

At this stage, however, we don't really need to know how the system works internally. We are instead, only concerned with how the system works from a high level.

Back in the first video we looked at how our data structures may appear once our system were fully implemented. I mentioned how those data structures would be useful to us when writing our Behat tests. Our User data, in JSON format, may look something like this:

{
  "id": 4,
  "email": "bob@tucker.com",
  "username": "bob"
}

It's important to have this - at least conceptually - in your head when designing your API.

Of course, the field names, field types, or overall structure may - and likely will - change as your API grows and evolves. Updating the Behat features is simple enough, and they ultimately serve as living documentation as to exactly how to interact with your system.

Initialising Behat in our Project

As we are using Symfony 3, there is a small change to the 'standard' way of working here.

In Symfony 2 we would have firstly installed Behat using Composer (as we did in the previous video) and then done:

bin/behat --init

However, in Symfony 3, any third party binaries now live in the vendor/bin directory, so our command is instead:

vendor/bin/behat --init

That's going to go ahead and create us some directories and a FeatureContext.php file, which I largely ignore. It will also create us a behat.yml file, which we will use to configure our Behat setup.

Honestly, working with Behat is a little strange at first. And I say "at first" as that's really where the strangeness lies. It's not too often, unless you frequently create a ton of new projects, that you will need to run a behat --init, and as such, your first time(s) can be a little confusing.

Thankfully, as you will see, once you are set up (or initialised), there's a set routine you will fall in to, which is quite formuliac, and easy to repeat. Again, your first few files with Behat (and PHPSpec, or any other test tool) will be the hardest, but once you get over that, it becomes very easy to continue working in that work flow.

Creating our User Feature

Personally, I like to create my Features directory inside the Bundle that the features relate too.

You can have a generic features directory - and that's how Behat will create the folder structure for you - but I have used this approach for a long while now, and I find it better fits my needs personally. At the end of the day, it's all about organisation - I like to keep my features near to my related code, rather than for the project as a whole.

As such, I manually create my own directory structure:

src/AppBundle/Features

and inside there:

src/AppBundle/Features/Context

Now, don't worry about the Context folder, or Contexts in general at this stage. We will cover that in a future video. We won't even need the Context folder to complete this current stage.

To create our first Behat feature, we need to create a new file:

src/AppBundle/Features/user.feature

I'd advise installing the Behat add-on for PHPStorm, if you are using PHPStorm. The behat add-on / plug-in will give you step / keyword auto-completion for your feature definitions, the ability to ctrl+click a step definition, and more. Well worth it - it's free!

Feature Definition

As I mention in the video, I'm not really doing this as a beginners guide to Behat. I am instead writing these features as I would do in the real world. I believe you will get the hang of this just as quickly by showing you this way, instead of a hand holding from the very basics.

All Behat feature files follow the same format.

We start off with a Feature definition at the top of the file. This is written in English (or your native language), and won't be interpreted / used at all by the computer / behat for testing.

Think of this Feature as being the same as if you were to walk in to a computer shop, pick up a software box, and look on the back for the bullet point list of features the software provides.

Feature: Manage Users data via the RESTful API

Next, the feature is explained in a little more depth using a "In order to..., As a..., I need to..." format.

Again, this won't be used by a computer in any way. This is for humans to explain the purpose of this feature.

  In order to offer the User resource via an hypermedia API
  As a client software developer
  I need to be able to retrieve, and update JSON encoded User resources

It's a little tricky writing these features on a single person project. If you are on a larger project, you would want to use the businesses own language to describe just what the heck this feature is all about.

However, in our case, we are developers talking to ourselves. So, for me, this works.

Also, it could be that you have layers of Behat features. Some talk about the business layer, but others like this, talk about how a specific part of the system works.

This is a touch controversial and may not be entirely BDD best practice. If you are at all interested in improving your understanding of BDD, I highly recommend this book, which I found easy enough to read (that is, not too technical / dry) and will definitely aid in understanding BDD as a whole. Please note, this is an affiliate link.

In truth, I found writing the feature outlines more challenging than writing the scenarios themselves!

Adding Background to your Features

If you've ever done any testing before, you will have come across the issue of test setup and tear down.

To ensure the test is fair we want to make sure the environment is in a known state before starting a test.

This stops a situation where the test run starts with a database containing three widgets, the first test deletes a widget, and the second test checks for the presence of three widgets... alas, now there are only two widgets. We don't want to maintain state between tests - or to put it another way, each individual test should start by recreating the database back to a known state.

In Behat, we do this using Background.

The background describes how the system looks before any test (or scenario) is run in our feature.

In our case, the background looks like this:

  Background:
    Given there are Users with the following details:
    | uid | username | email          | password |
    | u1  | peter    | peter@test.com | testpass |
    | u2  | john     | john@test.org  | johnpass |
    And there are Accounts with the following details:
    | uid | name     | users |
    | a1  | account1 | u1    |
    And I am successfully logged in with username: "peter", and password: "testpass"
    And when consuming the endpoint I use the "headers/content-type" of "application/json"

This is where things get a touch more advanced.

We are using Tables here to describe multiple entries / records / bits of data that we expect exist before our tests can properly run.

Behat uses regular expressions to match lines in English (or other native language) to functions in our test code. You don't need to know how this happens, and as you will see shortly, Behat will do the hard work of generating these function outlines for us if they don't actually exist.

In this example, when we run Behat - assuming everything was correctly configured - it would see our first line:

Given there are Users with the following details:

And promptly die on us, spitting out a function stub we can then copy / paste into a Context file. We then implement the code that tells the computer how to convert our English language into some code / database setup.

However, because we are passing in a table (a TableNode in Behat speak) we would also get access to all the tabular data we provided also. This would allow us to loop over the data line by line, splitting up the data into individual columns, and then build and persist entities based off the submitted data.

This may look like:

<?php

namespace AppBundle\Features\Context;

use Behat\Behat\Context\Context;
use Behat\Gherkin\Node\TableNode;
use Behat\Behat\Context\SnippetAcceptingContext;
use Doctrine\ORM\EntityManagerInterface;
use FOS\UserBundle\Model\UserManagerInterface;

class UserSetupContext implements Context, SnippetAcceptingContext
{
    /**
     * @var UserManagerInterface
     */
    protected $userManager;

    /**
     * @var EntityManagerInterface
     */
    protected $em;

    /**
     * UserContext constructor.
     * @param UserManagerInterface $userManager
     * @param EntityManagerInterface $em
     */
    public function __construct(UserManagerInterface $userManager, EntityManagerInterface $em)
    {
        $this->userManager = $userManager;
        $this->em = $em;
    }

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

            $user = $this->userManager->createUser();

            $user->setEnabled(true);
            $user->setUsername($val['username']);
            $user->setEmail($val['email']);
            $user->setPlainPassword($val['password']);

            $this->userManager->updateUser($user, true);

            $qb = $this->em->createQueryBuilder();

            $query = $qb->update('AppBundle:User', 'u')
                ->set('u.id', $qb->expr()->literal($val['uid']))
                ->where('u.username = :username')
                ->andWhere('u.email = :email')
                ->setParameters([
                    'username' => $val['username'],
                    'email' => $val['email']
                ])
                ->getQuery()
            ;

            $query->execute();
        }
    }
}

One interesting thing here is that instead of using integers for our ID's, I am using a GUID. The GUID itself would be some crazy long nonsense string - 6ccd780c-baba-1026-9564-0040f4311e29 - which is a real pain in the backside for testing.

Instead, I supply my own GUID which I can then re-use as needed for queries etc. That's why I set the u.id manually.

Here we use dependency injection to inject the UserManager, but you could also use the EntityManager directly also.

Again, the first time you have to write one of these is the hardest. The following times you would likely do mucho-copyo-pasteo.

Now, herein lies our first problem. Symfony 3 and Behat are currently not playing nicely together. So doing the dependency injection here is going to be tricky. Hopefully we will resolve this shortly.

I'll cover each of the individual background steps when we move into properly implementing our feature contexts. Again, I have deviated somewhat from the normal approach by having a UserSetupContext. This is a bit of a nonsense. But it works for me.

Scenarios

I will largely cover the scenarios in the next video, so at this stage I will suggest you watch the video and see the first few scenarios outlined with a description of what I'm doing. Then we will discuss the scenarios in more detail in the next video outline.

Here's how our user.feature looks at the end of this video:

Feature: Manage Users data via the RESTful API

  In order to offer the User resource via an hypermedia API
  As a client software developer
  I need to be able to retrieve, create, update, and delete JSON encoded User resources

  Background:
    Given there are Users with the following details:
    | uid | username | email          | password |
    | u1  | peter    | peter@test.com | testpass |
    | u2  | john     | john@test.org  | johnpass |
    And there are Accounts with the following details:
    | uid | name     | users |
    | a1  | account1 | u1    |
    And I am successfully logged in with username: "peter", and password: "testpass"
    And when consuming the endpoint I use the "headers/content-type" of "application/json"

  Scenario: User cannot GET a Collection of User objects
    When I send a "GET" request to "/users"
    Then the response code should 405

  Scenario: User can GET their personal data by their unique ID
    When I send a "GET" request to "/users/u1"
    Then the response code should 200
     And the response header "Content-Type" should be equal to "application/json; charset=utf-8"
     And the response should contain json:
      """
      {
        "id": "u1",
        "email": "peter@test.com",
        "username": "peter",
        "accounts": [
          {
            "id": "a1",
            "name": "account1"
          }
        ]
      }
      """

As with everything said so far, the hard part is this first feature. The next feature you write, and the one after that, and so on, will all feel more natural. You will have a handy reference (your first feature) to refer too, and things will be easier because of it.

I'll show you a good trick to writing these scenarios so you don't have to do too much thinking. But again, in Symfony 3 that's currently a lot harder due to package problems.

Code For This Course

Get the code for this course.

Episodes