Functional Testing in Symfony2

Taylor Ren
Tweet

In my previous article, we demonstrated how to load sample data into our Symfony development environment.

The test data may not be useful as it stands on its own. When coupled with Functional Testing, however, it becomes a life saver.

Functional Testing

Symfony’s official site has a useful document focusing on Unit Testing and Functional Testing. Be sure to take a look if you want to dig deeper into this topic.

I have covered Unit Tests to some extent in my previous article.

Unit Tests, in short, test the behavior of a class and/or its member functions. For example, I have created a Pagination class for my site to display all my book collections in pages, which also displays a list of pages for navigation purposes. To make the list of pages useful, the function must return an array of pages depending on the current page and the number of total pages so that the current page will be in the middle of the array. This is a built-in behavior of that function and does not relate to (but has to behave well when coupled with) different situations. That is where Unit Tests fit in.

If we look at testing the Pagination class in my earlier article, we can see very clearly how different scenarios are forged and the output tested:

            $paginator=new Paginator(2, 101, 10);
            $pages=$paginator->getTotalPages();
            $this->assertEquals($pages, 11);
            $list=$paginator->getPagesList();
            $this->assertEquals($list, array(1,2,3,4,5));

            $paginator=new Paginator(7, 101, 10);
            $list=$paginator->getPagesList();
            $this->assertEquals($list, array(5,6,7,8,9));

            $paginator=new Paginator(10, 101, 10);
            $list=$paginator->getPagesList();
            $this->assertEquals($list, array(7,8,9,10,11)); 

Functional Testing is different. We don’t look at the “correctness” of a single function, which should be verified by a Unit Test, but look at the bigger picture. The question answered by Functional Testing is: Is our app performing well in the sense that it displays the right content, corresponds to a user’s interaction, etc?

It can help us debug the app by repeating the user’s steps and reproducing the bug. If a user reports an error saying “When I do this, then that, with this data, the system is buggy”, we can simulate the operation sequence and find some deeply rooted errors.

Let’s see how to functionally test the site. Take a look at the site’s index page again. What should we test in this page’s content so that we are confident the site is performing as it should?

Of course, static content is none of our business as far as testing goes. Any errors in the static content are considered typos and are not in the scope of a functional test. It is therefore clear we will focus on the “dynamic” content and user interactions.

In this index page, we will focus on the following dynamic content based on our database’s sample data and app logic:

  • We should have a total 101 books in our library.
  • The latest book collected should have a purchase date of 1970-01-01 and its author is “Special“.
  • We should have 2 article collections (headlines) for my book readings and the latest review should be titled as “Review 2“.

Your own app’s logic and data may be different, but it is critical to identify this important information in the page’s content. I am using the above 5 bolded figures so that I can be sure what I’m looking for: my controller is doing the correct counting on books and headlines and also selecting the latest book / headline from the database.

The testing file to test the above conditions (or “assertions”) is located at: src/tr/rsywxBundle/Tests/Controller/DefaultControllerTest.php and excerpted below:

<?php

namespace tr\rsywxBundle\Tests\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class DefaultControllerTest extends WebTestCase
{

    public function testIndex()
    {
        $client = static::createClient();

        $crawler = $client->request('GET', '/');

//        $text = $crawler->text();
//        $fp = fopen('index.txt', 'w');
//        fwrite($fp, $text);
//        fclose($fp);

        $this->assertTrue($crawler->filter('html:contains("1970-01-01")')->count() == 1);
        $this->assertTrue($crawler->filter('html:contains("101 books")')->count() == 1);
        $this->assertTrue($crawler->filter('html:contains("by Special")')->count() == 1);
        $this->assertTrue($crawler->filter('html:contains("2 articles")')->count() == 1);
        $this->assertTrue($crawler->filter('html:contains("Review 2")')->count() == 1);

        //We click a link and go to the detail page
        $links=$crawler->selectLink('Special Book Title')->links();
        $link=$links[0];

        $crawler=$client->click($link);
}

We create an HTTP client, and create a crawler that simulates user action. In this test case, we want to visit the home page of the site, so we use $crawler = $client->request('GET', '/');.

Then, to make sure the page content contains this key information identified earlier, we make several assertions using assertTrue. For example, to translate “We should have total 101 books in our library” into:

$this->assertTrue($crawler->filter('html:contains("101 books")')->count() == 1);

we are expecting that there should be exactly one occurrence of “101 books” in the response. I use a longer phrase “101 books” to avoid some other “101” from being counted. If this assertion fails, it can either mean that there are some typos in the template or that there is a logical flaw and the counting of all books fails.

To run the test, issue the following command in the terminal window:

php phuunit.phar -c app/

The “-c app/” argument simply tells PHPUnit to use the configuration found in the app directory, which comes with the Symfony distribution.

Voilà! All tests passed! Seeing this green bar feels great. If any assertions fail, the bar will be red.

Let’s test some other aspects, too. How about the links? Are they doing what they’re supposed to (in terms of URI and bringing us to the right page)?

For example, now that we’re sure the page is displaying the latest book collected, one would expect that if we click on that link, it will bring us to the book detail page showing more detailed information regarding that book.

Let’s add a few more lines of code:

        //We click a link and go to the detail page
        $links=$crawler->selectLink('Special Book Title')->links();
        $link=$links[0];

        $crawler=$client->click($link);

        $this->assertTrue($crawler->filter('html:contains("ISBN: 123456789")')->count() == 1);
        $this->assertTrue($crawler->filter('html:contains("tag1")')->count() == 1);
        $this->assertTrue($crawler->filter('html:contains("tag2")')->count() == 1);
        $this->assertTrue($crawler->filter('html:contains("tag3")')->count() == 1);
        $this->assertTrue($crawler->filter('html:contains("tag4")')->count() == 1);
        $this->assertTrue($crawler->filter('html:contains("tag5")')->count() == 1);

In this code segment, we select the link with its text as “Special Book Title”. Based on our data fixture and app logic, there should be two of them in the index page. Either one shall bring us to the detail page of this book so I am using the first one:

By making the above additional assertions, we are sure we are viewing the book (the “special” book we created earlier) as identified by its ISBN and 5 tags. We are also sure that the link in the homepage correctly takes us to the book detail page showing the book listed in the homepage (as the latest collected book).

In the testing code above, I have commented out a few lines. These lines, if uncommented, will write the page content into a text file. During the testing process, there may be some weird behavior (e.g. you can see there is a string “Special Book Title” but the assertion of the existence of that same string simply fails). If that should occur, it is recommended to dump the response into a text file and search to see if there are uncommon issues.

Note: Symfony uses a special test_env (other than dev and prod) to run tests. We can even configure that environment to use a different, but similarly structured database. It is recommended to use a separate environment for pure testing purposes but for the simplicity of this article, we have skipped this process.

Conclusion

In this article, we covered running functional tests using PHPUnit in a Symfony app. A functional test alone won’t be very meaningful as there isn’t enough data to populate our pages in a more meaningful manner. Data fixtures come in handy for automating the data loading process and more importantly, populating the data in a controlled way so that we can make further assertions on the expected output.

Feel free to comment and we will be happy to cover this topic in greater detail if you are interested.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • Piotr Sledz

    It is worth to consider using mockups rather than DataFixtures. Especially when you have to test a lot of records and for every test you need “fresh” data.

    • Taylor Ren

      They do not exclude each other. Depend on various scenarios, various combinations can be considered.

  • Andrej Sramko

    nice article. I wonder how do you define in which order will be the functional test cases executed, if you have more of them?