Our First Passing Behat User Scenario


By the end of this video, we will have our first passing Behat test for our User feature. Hoorah, it has only taken us 11 videos to get here.

But mild sarcasm aside, we have laid a strong foundation to ensure our resulting application will behave as we expect.

We've created code to populate our database dynamically, yet predictably. We've also added the vast majority of code that Behat will need to interact with, and validate our API behaves properly, and provides the expected return values from our various end points. Not to mention setting up all our dependencies and configuration, created our routing structure and our User entity...

Although straying somewhat from the point of this video, it is worth realising at this point that even a simple venture into building a tested API still takes a very long time, relative to hacking up a prototype without any testing. I believe it is worthwhile, if the end result will be meaningful beyond the prototype stage.

The First Test Scenario

As we've covered these steps in previous videos already, I am going to only look at the new parts.

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"

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

I've commented out all but the User entity setup part of the Background step.

At this stage we don't have the concept of Account, we haven't secured our routes so don't need to log in, and as we aren't sending in a request that has a body (think POST, PUT, or PATCH requests), we don't need to set a Content-type.

The @t is a Behat tag. This isn't that big of a deal as I've also commented out every other scenario in the User feature. As our project grows, however, commenting out every test we aren't currently working on would be a right pain. So, instead, we can use Behat tags to run only specific tests we are interested in.

The purpose of @t is simply to mean @this. I don't want to keep typing @this or @failing, so I frequently use the tags of @t and @f to mark tests that I am interested in. You can use tags for all sort of other reasons, but this is a handy technique for now.

To run our test suite and make use of tags we could then do:

vendor/bin/behat --tags=t
# or 
vendor/bin/behat --tags=f

A Closer Look Behind The Scenario Steps

As covered in the previous video, we have already 'written' the vast majority of the code that we will need to run our Behat features.

There are only two steps in this first test, but we will use these steps over and over.

// /src/AppBundle/Features/Context/RestApiContext.php

    /**
     * Sends HTTP request to specific relative URL.
     *
     * @param string $method request method
     * @param string $url    relative url
     *
     * @When /^(?:I )?send a "([A-Z]+)" request to "([^"]+)"$/
     */
    public function iSendARequest($method, $url)
    {
        $url = $this->prepareUrl($url);
        $this->request = $this->getClient()->createRequest($method, $url);
        if (!empty($this->headers)) {
            $this->request->addHeaders($this->headers);
        }

        $this->sendRequest();
    }

I would strongly encourage you to copy and paste the RestApiContext from the previous video and have a look at this code in your IDE.

This will allow you to ctrl+click things, see what is happening, and answer a few questions that will likely pop up when reading this code for the first time.

$url = $this->prepareUrl($url);

This will, behind the scenes - via private methods (read the code!) - replace any placeholders passed in with the value in use during the current test run. Placeholders are not something we have, or will be covering, so if interested to know more, be sure to read the official Behat docs.

$this->request = $this->getClient()->createRequest($method, $url);

Next, we use Guzzle to create a request of the type we are passing in (GET in this instance), and also telling Guzzle which $url to send this request too (/users). Behind the scenes, Guzzle will prepend the base_url we set up in our CSA Guzzle client config to ensure the correct absolute URL is used for this test run.

If we have any headers, they will be added next:

if (!empty($this->headers)) {
    $this->request->addHeaders($this->headers);
}

We will use headers to ensure we send in the correct Content-type with our request, but not in this particular test.

Finally, we send the request. This is actually a private function of the RestApiContext, and will store the response from this request on $this->response;:

    private function sendRequest()
    {
        try {
            $this->response = $this->getClient()->send($this->request);
        } catch (RequestException $e) {
            $this->response = $e->getResponse();

            if (null === $this->response) {
                throw $e;
            }
        }
    }

Why do we store the response?

Well, because we care about the response. And we can use it to check things went according to our expections.

We could... check the response code was 200 for example:

// /src/AppBundle/Features/Context/RestApiContext.php

    /**
     * Checks that response has specific status code.
     *
     * @param string $code status code
     *
     * @Then /^(?:the )?response code should be (\d+)$/
     */
    public function theResponseCodeShouldBe($code)
    {
        $expected = intval($code);
        $actual = intval($this->response->getStatusCode());
        Assertions::assertSame($expected, $actual);
    }

We use PHPUnit for our assertions:

use PHPUnit_Framework_Assert as Assertions;

And then call the various assertion methods statically, because they are static methods :)

Assertions::assertSame($expected, $actual);

Creating the User Controller

At this point, we can run our first behat test but it will promptly fail.

We need to create our UserController and implement the required methods to make this work.

We are using FOSRESTBundle for our API here, and we will be using Automatic Route Generation. This simply means we need to follow the expected pattern when naming our controller actions, and by doing so, FOSRESTBundle will handle most everything else for us.

Let's set up the controller now:

<?php

namespace AppBundle\Controller;

use FOS\RestBundle\View\View;
use FOS\RestBundle\Controller\Annotations;
use FOS\RestBundle\View\RouteRedirectView;
use FOS\RestBundle\Controller\FOSRestController;
use FOS\RestBundle\Routing\ClassResourceInterface;
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Form\FormTypeInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

/**
 * Class UsersController
 * @package AppBundle\Controller
 * @Annotations\RouteResource("users")
 */
class UsersController extends FOSRestController implements ClassResourceInterface
{
    /**
     * Get a single User.
     *
     * @ApiDoc(
     *   output = "AppBundle\Entity\User",
     *   statusCodes = {
     *     200 = "Returned when successful",
     *     404 = "Returned when not found"
     *   }
     * )
     *
     * @param int   $userId     the user id
     *
     * @throws NotFoundHttpException when does not exist
     *
     * @return View
     */
    public function getAction($userId)
    {
        $user = $this->getDoctrine()->getRepository('AppBundle:User')->find($userId);

        $view = $this->view($user);

        return $view;
    }

    /**
     * Gets a collection of Users.
     *
     * @ApiDoc(
     *   output = "AppBundle\Entity\User",
     *   statusCodes = {
     *     405 = "Method not allowed"
     *   }
     * )
     *
     * @throws MethodNotAllowedHttpException
     *
     * @return View
     */
    public function cgetAction()
    {
        throw new MethodNotAllowedHttpException([], "Method not allowed");
    }
}

I'm not going to cover the controller set up in great detail because I've already covered this in the RESTful APIs with FoSRESTBundle tutorial series already.

One thing to point out here:

The contents of the getAction will change. This is a simplistic, naive implementation - the bare minimum - the get this test to pass. Later tests will require this method be re-written. This is one of the benefits of TDD, or testing if not test driven development in it's purest form.

Testing here gives us the opportunity to write the simplest implementation to make this test pass. If we have no further use cases for this method, there is no point refining it. The tests tell us when we are done, and if we do anything in the future that breaks the test, we can catch that also. But if the test is green, then move on to the next test.

At this stage, our first behat scenario should be passing. And that is good enough for us, for now :)

Code For This Course

Get the code for this course.

Episodes