Loïc Faugeron Technical Blog

Master Symfony2 - part 2: Test Driven Development 13/08/2014

Deprecated: This series has been re-written - see The Ultimate Developer Guide to Symfony

This is the second article of the series on mastering the Symfony2 framework. Have a look at the first one: Bootstraping.

In the first article we bootstraped our project with the following files:

.
├── app
│   ├── AppKernel.php
│   ├── cache
│   │   └── .gitkeep
│   ├── config
│   │   ├── config_prod.yml
│   │   ├── config_test.yml
│   │   ├── config.yml
│   │   ├── parameters.yml.dist
│   │   └── routing.yml
│   ├── logs
│   │   └── .gitkeep
│   └── phpunit.xml.dist
├── composer.json
├── composer.lock
├── src
│   └── Fortune
│       └── ApplicationBundle
│           └── FortuneApplicationBundle.php
└── web
    └── app.php

Here's the repository where you can find the actual code.

In this one we'll implement the first User Story, by writing tests first.

Note: writing tests before writing any code is part of the Test Driven Development (TDD) methodology.

Defining the User Story

With the help of our Scrum Master, our Product Owner (Nostradamus) managed to write the following user story:

As a User
I want to be able to submit a new quote
In order to make it available

Writing the test

Our first reflex will be to write a functional test. First create the directory:

mkdir -p src/Fortune/ApplicationBundle/Tests/Controller

Then the test class:

<?php
// File: src/Fortune/ApplicationBundle/Tests/Controller/QuoteControllerTest.php

namespace Fortune\ApplicationBundle\Tests\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\HttpFoundation\Response;

class QuoteControllerTest extends WebTestCase
{
    private function post($uri, array $data)
    {
        $headers = array('CONTENT_TYPE' => 'application/json');
        $content = json_encode($data);
        $client = static::createClient();
        $client->request('POST', $uri, array(), array(), $headers, $content);

        return $client->getResponse();
    }

    public function testSubmitNewQuote()
    {
        $response = $this->post('/api/quotes', array('content' => '<KnightOfNi> Ni!'));

        $this->assertSame(Response::HTTP_CREATED, $response->getStatusCode());
    }
}

Configuring the route

Now we need to link the /quotes URL to a controller, so let's edit the configuration:

# File: app/config/routing.yml
submit_quote:
    path: /api/quotes
    methods:
        - POST
    defaults:
        _controller: FortuneApplicationBundle:Quote:submit

Creating the controller

There wasn't any controllers until now, so we create the directory:

mkdir src/Fortune/ApplicationBundle/Controller

And finally the controller class:

<?php
// File: src/Fortune/ApplicationBundle/Controller/QuoteController.php

namespace Fortune\ApplicationBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;

class QuoteController extends Controller
{
    public function submitAction(Request $request)
    {
        $postedContent = $request->getContent();
        $postedValues = json_decode($postedContent, true);

        $answer['quote']['content'] = $postedValues['content'];

        return new JsonResponse($answer, Response::HTTP_CREATED);
    }
}

Now let's run our tests:

./vendor/bin/phpunit -c app

All green! This makes us confident enough to commit our work:

git add -A
git commit -m 'Created submission of quotes'

Testing bad cases

The submitted content shouldn't be empty. Let's add a test for the bad cases:

<?php
// File: src/Fortune/ApplicationBundle/Tests/Controller/QuoteControllerTest.php

namespace Fortune\ApplicationBundle\Tests\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\HttpFoundation\Response;

class QuoteControllerTest extends WebTestCase
{
    private function post($uri, array $data)
    {
        $headers = array('CONTENT_TYPE' => 'application/json');
        $content = json_encode($data);
        $client = static::createClient();
        $client->request('POST', $uri, array(), array(), $headers, $content);

        return $client->getResponse();
    }

    public function testSubmitNewQuote()
    {
        $response = $this->post('/api/quotes', array('content' => '<KnightOfNi> Ni!'));

        $this->assertSame(Response::HTTP_CREATED, $response->getStatusCode());
    }

    public function testSubmitEmptyQuote()
    {
        $response = $this->post('/api/quotes', array('content' => ''));

        $this->assertSame(Response::HTTP_UNPROCESSABLE_ENTITY, $response->getStatusCode());
    }

    public function testSubmitNoQuote()
    {
        $response = $this->post('/api/quotes', array());

        $this->assertSame(Response::HTTP_UNPROCESSABLE_ENTITY, $response->getStatusCode());
    }
}

Checking bad cases

Now let's fix the new tests:

<?php
// File: src/Fortune/ApplicationBundle/Controller/QuoteController.php

namespace Fortune\ApplicationBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;

class QuoteController extends Controller
{
    public function submitAction(Request $request)
    {
        $postedContent = $request->getContent();
        $postedValues = json_decode($postedContent, true);

        if (empty($postedValues['content'])) {
            $answer = array('message' => 'Missing required parameter: content');

            return new JsonResponse($answer, Response::HTTP_UNPROCESSABLE_ENTITY);
        }
        $answer['quote']['content'] = $postedValues['content'];

        return new JsonResponse($answer, Response::HTTP_CREATED);
    }
}

Finally run the tests:

./vendor/bin/phpunit -c app

All green! Let's call it a day and commit our work:

git add -A
git commit -m 'Managed submission of empty/no quotes'

Conclusion

For those who didn't practice a lot with Symfony2, this article should have demonstrated how quick and simple it is to implement the first User Story (test and code alike).

In the next article, we'll learn how to work with services.