Functional Tests

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $10.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

The last type of test is called a functional test. And it's way different than what we've seen so far. With unit and integration tests, we call methods on our code and test the output. But with a functional test, you command a browser, which surfs to your site, clicks on links, fills out forms and asserts things it sees on the page. Yep, you're testing the interface that your users actually use.

Oh, and functional tests also apply if you're building an API. It's the same idea: you would use an HTTP client to make real HTTP requests to your API and assert the output.


First, to give us some magic, I want to install a special bundle: Google for LiipFunctionalTestBundle.

This bundle is not needed to write functional tests. But, it has a collection of optional, extra goodies!

Copy the composer require line, move over to your terminal, and paste:

composer require --dev liip/functional-test-bundle


If you are using PHPUnit 7+ or Symfony 4, you need to require version 2.0 of this bundle. At this time, 2.0 is still alpha, and needs to be installed specifically with: composer require --dev liip/functional-test-bundle:~2.0@alpha

If you're using Symfony 3, make sure you've installed PHPUnit 6.3 and install version 1 of this bundle so that everything can play together nicely: composer require --dev liip/functional-test-bundle:^1.9

Functional Test Setup

Functional tests look like unit tests at first: they use PHPUnit in the exact way we've been seeing. But instead of writing one test class per PHP class, you'll usually create one test class per controller class.

It doesn't have much yet, but we're going to functionally test our homepage. Since the code behind this lives in DefaultController, let's create a Controller directory in tests and add a new DefaultControllerTest class.

But now, instead of extending TestCase or KernelTestCase, extend WebTestCase. But wait! There are two! The normal base class is the one from FrameworkBundle. It actually extends KernelTestCase, which means we have all the same tools as integration tests. But, it adds a few methods to help create a client object: a special object we'll use to make requests into our app.

Today we'll choose WebTestCase from LiipFunctionalTestBundle. No surprise, this class itself extends the normal WebTestCase. Then, it adds a bunch of optional magic.

... lines 1 - 4
use Liip\FunctionalTestBundle\Test\WebTestCase;
class DefaultControllerTest extends WebTestCase
... lines 9 - 16

TDD & The Functional Test

Let's add the first test: public function testEnclosuresAreShownOnTheHomepage(). Right now, the homepage is empty. But in a minute, we're going to render all of the enclosures. So let's do a little TDD testing! Start by creating a client with $client = $this->makeClient(). This method comes from LiipFunctionalTestBundle, but is just a wrapper around Symfony's normal static::createClient(). The version from the bundle just adds some optional authentication magic.

... lines 1 - 6
class DefaultControllerTest extends WebTestCase
public function testEnclosuresAreShownOnHomepage()
... line 10
$client = $this->makeClient();
... lines 12 - 15

Next, make a request! $crawler = $client->request('GET', '/') to go to the homepage. We'll talk more about this Crawler object in a few minutes. Then, the simplest test is to say $this->assertStatusCode(200) and pass $client. But even this is just a shortcut to make sure 200 matches $client->getResponse()->getStatusCode().

... lines 1 - 8
public function testEnclosuresAreShownOnHomepage()
... lines 10 - 12
$crawler = $client->request('GET', '/');
$this->assertStatusCode(200, $client);
... lines 17 - 18

And yea... the first part of the test is done! This at least makes sure our page isn't broken!

Finishing Installing LiipFunctionalTestBundle

But before we try it, we need to finish installing the bundle. Copy the 3 bundle lines, open AppKernel and paste them there.

... lines 1 - 6
class AppKernel extends Kernel
public function registerBundles()
... lines 11 - 21
if (in_array($this->getEnvironment(), ['dev', 'test'], true)) {
... lines 23 - 31
if ('test' === $this->getEnvironment()) {
$bundles[] = new LiipFunctionalTestBundle();
... lines 36 - 37
... lines 39 - 58

We also need to add one line to config_test.yml.

... lines 1 - 27
liip_functional_test: ~

If you're using Symfony Flex, these steps should eventually be done for you. I say eventually, because - at this moment - Symfony 4 support is still being added to the bundle.

Ok! Let's try it! Find your terminal, run phpunit, and point it at the new controller:

./vendor/bin/phpunit tests/AppBundle/Controller/DefaultControllerTest.php

Yes! It passes!

Symfony's Client Versus Mink Versus Others

But... what just happened exactly? Did our code just make a real HTTP request to our app... just like if we refreshed it in the browser? Well... not quite.

Remember: in a functional test, we use PHP to command a browser and tell it to go to a page, click on links and fill out forms. But, there are multiple libraries that give us this superpower. We're using Symfony's BrowserKit client... mostly because it's built into Symfony and easy to start using. But, there are others. My favorite is called Mink, which is used behind the scenes with Behat. We have a tutorial all about Behat, with big sections devoted to Mink. So, check that out.

Go Deeper!

Learn all about Mink and Behat:

So... what's the difference between Symfony's BrowserKit and Mink? With BrowserKit, you're not actually making a real HTTP request to your app. Nope, you're making a "fake" request directly into your code. Honestly, that doesn't matter much. And actually, it makes setup a bit easier: we didn't need to configure that our site lived at http://localhost:8000 or anything like that.

But, BrowserKit has one big disadvantage: you can't test JavaScript functionality. Nope, since these are fake requests, your JavaScript simply doesn't run! This is the main reason why I prefer Mink: it does allow you to run your code in a real browser... with JavaScript.

For the next few chapters, we are going to use Symfony's BrowserKit Client. But, most of the concepts transfer well to other test clients, like Mink. And you'll still be able to use most of the magic from LiipFuntionalTestBundle. If you have questions about this, just ask in the comments!

Next, let's talk about this $crawler object and how we can use it to dive into the HTML of the page!

Leave a comment!

  • 2020-07-15 Diego Aguiar

    Cool! I'm glad to hear you could find what's going on here. Thanks for sharing it.

  • 2020-07-15 yvon Huynh

    I think I understood why I couldn't get the text of login page, because with the static::createClient() with the credential , you are already logged in, no need to post credential via submit !

  • 2020-07-06 yvon Huynh

    Hi thank you for your tip, unfortunately I am not in symfony 5 and the app is already in production, I wouldn't date making that change. I will visit the logout page prior to do anything else, or empty a cache. I think it would be interesting to put that in tearDown function

  • 2020-07-03 Diego Aguiar

    Hey yvon Huynh

    The only difference between your first attempt to submit the form and the later one is that you forgot to call ->form() after filtering the response e.g. $crawler->filter('#loginForm')->form();

    > The problem now is I don't know how to log out, the session stays active between two tests.
    Really? I wouldn't expect that behavior but what you can do is just to hit your logout path, that should do the work

    by the way, check out this post
    for your subsequent tests it would be better to just "simulate" the login and perform the action under test


  • 2020-07-03 yvon Huynh

    Update : some how I managed to do the login with :
    $form = $crawler->filter('html > body > div > form')->form();
    The problem now is I don't know how to log out, the session stays active between two tests.

    Hi everybody, I had a hard time making a basic functional test work, I need to login in the first place. I use symfony 4.4, I find the different tutorials on internet confusing, many ways to do thing..., here is my code (symfony CssSelector is installed):
    public function testLoginToApp()

    $client = static::createClient([], [
    'PHP_AUTH_USER' => '',
    'PHP_AUTH_PW' => 'mypass',

    $crawler = $client->request('GET', '/login');
    $form = $crawler->filter('#loginForm');
    $crawler = $client->followRedirect();

    // $this->assertEquals(302, $client->getResponse()->getStatusCode());


    I just want to login and test if I can access certain routes.
    However the nodelist is empty, it cannot find the button# to post data. What did I do wrong?

  • 2020-04-20 Vladimir Sadicov

    Hey Julien Bonnier

    Yeah sometimes it can be useful :))))

    We are glad that you solved your issue! Feel free to ask any questions!


  • 2020-04-20 Julien Bonnier

    Wow I never look to the content below the videos, just saw the *tip*. Answer is: liip/functional-test-bundle:^1.9

  • 2020-04-20 Julien Bonnier

    I've been trying to install liip's bundle for more than an hour now. I'm giving up this course for tonight but can anyone please provide a version number that should install?

    composer/ca-bundle 1.2.7 Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.
    doctrine/annotations 1.10.1 Docblock Annotations Parser
    doctrine/cache 1.10.0 PHP Doctrine Cache library is a popular cache implementation that supports many different drivers such as redis, memcache, apc, mongodb and ...
    doctrine/collections 1.6.4 PHP Doctrine Collections library that adds additional functionality on top of PHP arrays.
    doctrine/common 2.12.0 PHP Doctrine Common project is a library that provides additional functionality that other Doctrine projects depend on such as better reflec...
    doctrine/data-fixtures 1.4.2 Data Fixtures for all Doctrine Object Managers
    doctrine/dbal v2.10.1 Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.
    doctrine/doctrine-bundle 1.10.3 Symfony DoctrineBundle
    doctrine/doctrine-cache-bundle 1.3.5 Symfony Bundle for Doctrine Cache
    doctrine/event-manager 1.1.0 The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.
    doctrine/inflector 1.3.1 Common String Manipulations with regard to casing and singular/plural rules.
    doctrine/instantiator 1.3.0 A small, lightweight utility to instantiate objects in PHP without invoking their constructors
    doctrine/lexer 1.2.0 PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.
    doctrine/orm v2.7.2 Object-Relational-Mapper for PHP
    doctrine/persistence 1.3.7 The Doctrine Persistence project is a set of shared interfaces and functionality that the different Doctrine object mappers share.
    doctrine/reflection 1.2.1 The Doctrine Reflection project is a simple library used by the various Doctrine projects which adds some additional functionality on top of...
    fig/link-util 1.1.0 Common utility implementations for HTTP links
    incenteev/composer-parameter-handler v2.1.4 Composer script handling your ignored parameter file
    jdorn/sql-formatter v1.2.17 a PHP SQL highlighting library
    monolog/monolog 1.25.3 Sends your logs to files, sockets, inboxes, databases and various web services
    myclabs/deep-copy 1.9.5 Create deep copies (clones) of your objects
    ocramius/package-versions 1.4.2 Composer plugin that provides efficient querying for installed package versions (no runtime IO)
    paragonie/random_compat v9.99.99 PHP 5.x polyfill for random_bytes() and random_int() from PHP 7
    phar-io/manifest 1.0.1 Component for reading manifest information from a PHP Archive (PHAR)
    phar-io/version 1.0.1 Library for handling version information and constraints
    phpdocumentor/reflection-common 2.0.0 Common reflection classes used by phpdocumentor to reflect the code structure
    phpdocumentor/reflection-docblock 5.1.0 With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a Doc...
    phpdocumentor/type-resolver 1.1.0 A PSR-5 based resolver of Class names, Types and Structural Element Names
    phpspec/prophecy v1.10.3 Highly opinionated mocking framework for PHP 5.3+
    phpunit/php-code-coverage 5.3.2 Library that provides collection, processing, and rendering functionality for PHP code coverage information.
    phpunit/php-file-iterator 1.4.5 FilterIterator implementation that filters files based on a list of suffixes.
    phpunit/php-text-template 1.2.1 Simple template engine.
    phpunit/php-timer 1.0.9 Utility class for timing
    phpunit/php-token-stream 2.0.2 Wrapper around PHP's tokenizer extension.
    phpunit/phpunit 6.3.0 The PHP Unit Testing framework.
    phpunit/phpunit-mock-objects 4.0.4 Mock Object library for PHPUnit
    psr/cache 1.0.1 Common interface for caching libraries
    psr/container 1.0.0 Common Container Interface (PHP FIG PSR-11)
    psr/link 1.0.0 Common interfaces for HTTP links
    psr/log 1.1.3 Common interface for logging libraries
    psr/simple-cache 1.0.1 Common interfaces for simple caching
    sebastian/code-unit-reverse-lookup 1.0.1 Looks up which function or method a line of code belongs to
    sebastian/comparator 2.1.3 Provides the functionality to compare PHP values for equality
    sebastian/diff 2.0.1 Diff implementation
    sebastian/environment 3.1.0 Provides functionality to handle HHVM/PHP environments
    sebastian/exporter 3.1.2 Provides the functionality to export PHP variables for visualization
    sebastian/global-state 2.0.0 Snapshotting of global state
    sebastian/object-enumerator 3.0.3 Traverses array structures and object graphs to enumerate all referenced objects
    sebastian/object-reflector 1.1.1 Allows reflection of object attributes, including inherited and non-public ones
    sebastian/recursion-context 3.0.0 Provides functionality to recursively process PHP variables
    sebastian/resource-operations 1.0.0 Provides a list of PHP built-in functions that operate on resources
    sebastian/version 2.0.1 Library that helps with managing the version number of Git-hosted PHP projects
    sensio/distribution-bundle v5.0.25 Base bundle for Symfony Distributions
    sensio/framework-extra-bundle v3.0.29 This bundle provides a way to configure your controllers with annotations
    sensio/generator-bundle v3.1.7 This bundle generates code for you
    sensiolabs/security-checker v5.0.3 A security checker for your composer.lock
    swiftmailer/swiftmailer v5.4.12 Swiftmailer, free feature-rich PHP mailer
    symfony/monolog-bundle v3.2.0 Symfony MonologBundle
    symfony/polyfill-apcu v1.15.0 Symfony polyfill backporting apcu_* functions to lower PHP versions
    symfony/polyfill-ctype v1.15.0 Symfony polyfill for ctype functions
    symfony/polyfill-intl-icu v1.15.0 Symfony polyfill for intl's ICU-related data and classes
    symfony/polyfill-mbstring v1.15.0 Symfony polyfill for the Mbstring extension
    symfony/polyfill-php56 v1.15.0 Symfony polyfill backporting some PHP 5.6+ features to lower PHP versions
    symfony/polyfill-php70 v1.15.0 Symfony polyfill backporting some PHP 7.0+ features to lower PHP versions
    symfony/polyfill-util v1.15.0 Symfony utilities for portability of PHP codes
    symfony/swiftmailer-bundle v2.6.7 Symfony SwiftmailerBundle
    symfony/symfony v3.3.18 The Symfony PHP framework
    theseer/tokenizer 1.1.3 A small library for converting tokenized PHP source code into XML and potentially other formats
    twig/twig v2.12.5 Twig, the flexible, fast, and secure template language for PHP
    webmozart/assert 1.8.0 Assertions to validate method input/output with nice error messages.

  • 2019-02-04 Leanna Pelham

    Hey Nicolás González Flores ! Thanks again for reporting this issue, we've updated the script and added a note to the video. You rock! 👏

  • 2018-09-17 weaverryan

    Hey Nicolás González Flores!

    Thanks for the pointer on this! We'll look into adding a note so other people don't hit this :).


  • 2018-09-15 Nicolás González Flores

    Hello guys,

    in case you are using Symfony 3.3 and PHPUnit 7 or later (the version that will be installed if you follow this tutorial at this moment), you may end in a limbo of compatibility with liip. Make sure you are using the same version of PHPUnit that this tutorial is using, the version 6.3. Then you won't have any problem installing liip.


  • 2018-06-11 Diego Aguiar

    Usually you don't, but if you don't care about *that* service (maybe it's just a dependency that you are not exercicing in that test), then you can mock it out.

  • 2018-06-09 Coder

    Hi, so in functgional test we still mock some classes?

  • 2018-06-04 Diego Aguiar

    Hey Coder

    Good question, an end-to-end test is when you test the entire flow of a feature in your system, in other words, you test that all your components communicates as expected, in this kind of tests you don't mock anything, if you have to store something in the DB, you configure a test DB, if you have to hit an external resource like an API, first you have to find if they offer a sandbox endpoint, if not, then you would have to create a test account so you can use those credentials in your tests


  • 2018-06-02 Coder

    if when clicks buttons it is called functional test, then what is end to end test?