Testing Symfony Forms with Complex Field Types

Introduction

Creating functional tests for controllers that have forms with complex field types can be hard to understand at first, but once you understand the basic concepts, they become easier. This article explains a simple example that I had to test with an application i developed.

Client Crawler and Redirects

For my application, I needed to click on a link to get to the authentication page, and then after authentication succeeds, that controller redirects back to the homepage (with cookies created). Since a typical test case looks for a 200 response code, when redirected, you will actually get a 302 response code instead. To workaround this in Symfony, you’ll need to follow redirects. Sample client code would look like the following:

...
public function testLogin(){
    $client = static::createClient(); // Create crawler client.
    $client->followRedirects(); // After authenticating will redirect.
    ...

So in the above code, the followRedirects() method is used when your client is expecting to be redirected on a successful response as per the documentation.

Getting Form Page and Node

After creating the client, I needed to click a link to go to the login page and then assert that it is the correct page and get a crawler node (the login button). Example code is as follows:

...
$crawler = $client->request('GET', '/'); // Get homepage.
$loginLink = $crawler->selectLink('Login link')->link();
$crawler = $client->click( $loginLink ); // Click link.

$this->assertContains(
    'Enter your details',
    $client->getResponse()->getContent()
);

$authCrawlerNode = $crawler->selectButton('Login'); // Get form button.
...

The above code gets the homepage content, looks for a link called “Login link” then clicks it; and then finally asserts the resultant page contains “Enter your details”. Then it gets the crawler node a button labelled “Login”.

The Login Form

My form is interesting in that it asks for first & last names and the person’s date of birth. See the screenshot below:

Authenticate

Notice the date of birth is a drop-down list. This is a complex type of input I was mentioning at the beginning of this article.

The Symfony testing documentation only briefly describes how to perform testing on some complex form input types, so you really have to experiment to figure things out. For example, in my case, based on the documentation I could write the following example code:

...
$form = $authCrawlerNode->form(); // Get the form.

// Fill in the Authentication form details.
$form['form[f_name]'] = 'John';
$form['form[l_name]'] = 'Doe';
$form['form[dob][day]']->select('2');
$form['form[dob][month]']->select('7');
$form['form[dob][year]']->select('1981');
...

However, that’s not the easiest way to write the code.

You may wonder how I figured out the form input names to use and the values. If you use FireFox, right-click on the day drop-down list element and select “Inspect element” and you’ll see something like the following:

dob_day_inspect

Notice the name value. You can copy and paste that into your code. The name is a composite of the form name + form field type name + list item. So in my case, my form was called “form”, and the field type name is “dob”, and the list item is the “day” field.

Easier Way to Write Code

An easier way to write the code is put the values you want to enter in the form in an array, and use the crawler node to do this like so:

...
$form = $authCrawlerNode->form(array(
      'form[f_name]' => 'John',
      'form[l_name]' => 'Doe',
      'form[dob][day]' => '2',
      'form[dob][month]' => '7',
      'form[dob][year]' => '1981'
));

$crawler = $client->submit( $form );
...

Notice you don’t call the select method on the drop-down list items, but just enter the correct values you need.

You can simplify even further by just using similar code and the client submit method like so:

...
$client->submit( $authCrawlerNode->form(array(
     'form[f_name]' => 'John',
     'form[l_name]' => 'Doe',
     'form[dob][day]' => '2',
     'form[dob][month]' => '1',
     'form[dob][year]' => '1981'
)));
...

Verifying Submission

Finally after submitting the form, you need to assert that the user was indeed authenticated. The code would look like the following:

   ...
   $client->submit( $authCrawlerNode->form(array(...
   ...

   $this->assertEquals(200, $client->getResponse()->getStatusCode());
}

Since the follow redirects has been enabled, then then expected response code of 200 should be expected. You should verify any failures of the test case. A lot of times it will be the incorrect array values specified. All the above code is from working real examples.

I hope this is a good example for someone to use.

Using Cookies with PHPUnit and Symfony Basic Client

Introduction

I was writing and running some simple PHPUnit functional tests using the Symfony basic client (as described here), and after authenticating (submitting a form), I noticed the client didn’t have any cookies set; or at least it appears that way. Then later after a lot of struggling, I realized that it is just a simple crawler (simple browser) and that I needed to set the cookies on my test code side. This article tells you what I did.

Creating a Client and Getting a Response

To create a client in a class that extends WebTestCase, you simply call this code:

$client = static::createClient();

This creates a client, and then you can do various things with the client. For example, on the local machine you might want to get the homepage (in this case “/”) like so:

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

This works fine, except for one key point: I my case, the Symfony controller (actually the twig file) was expecting certain cookies to be set in order to know that the user had already authenticated. With cookies set, then the response returned by the controller would contain different content. In my application it is where a student has filled in a form and then a cookie gets stored in the browser, the browser then maintains the cookie.

Using the Symfony client using the above 2 commands would not return the expected content, since the cookies need to first be set.

Setting Cookies

To set a cookie, we first need to get a CookieJar (holds cookies), also make sure you keep the CookieJar away from the Cookie Monster. We get the CookieJar from the client like so:

$client->getCookieJar();

The CookieJar has a set method, where we can pass in a cookie. We can combine this with the above and issue a command like so:

$client->getCookieJar()->set( $cookie );

Where in the above line, $cookie is a Cookie object.

Final Running Code

My final code looks like the following:

<?php
// tests/AppBundle/Functional/DefaultControllerTest.php
namespace tests\AppBundle\Functional;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\BrowserKit\Cookie;

class DefaultControllerTest extends WebTestCase
{
   ...
   public function testWithCookies(){
      $client = static::createClient();
      $client->getCookieJar()->set( new Cookie('form_complete', '1', strtotime('+1 day')) );
      $client->getCookieJar()->set( new Cookie('consent_complete', '1', strtotime('+1 day')) );

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

      $this->assertContains(
            'Consent for Release of',
            $client->getResponse()->getContent()
      );
   }
}

In the above notice the use statement for the Cookie (you need that), and also I’ve combined setting the cookie with the getCookieJar->set() method. Then after I perform my PHPUnit assertion of the expected content.

Hoepfully this helps you, as I struggle for a while on this.

PHPUnit Code Coverage in Symfony

Introduction

I’ve been creating some unit tests and functional tests for a Symfony application that I’m currently developing and I wanted to share some of those experiences. PHPUnit has a number command line options that you can specify when running it, including a very useful “–coverage-text” option which is what I show here.

Creating Test Code

It’s a good idea to create both unit and functional tests. Unit tests should be used to test any of your Symfony Entities, and Function tests would test the controller, the web pages as part of your application, and any forms that you may have.

Here is some sample code, that creates a Client object and calls 6 of its methods (getters and setters) and performs assertions:

public function testCreateClient(){
   $client = new Client(); // Create a Client object.
   $client->setUserAgent("My Browser Type");
   $client->setClientIP("192.168.100.1");
   $now = new \DateTime("now");
   $client->setTimeSubmit( $now );

   // Verify settings.
   $this->assertEquals("My Browser Type", $client->getUserAgent());
   $this->assertEquals("192.168.100.1", $client->getClientIP());
   $this->assertEquals($now, $client->getTimeSubmit());
}

The above code essentially creates a Client object and then sets various properties of the object and then later gets the properties of the object to perform assertions.

Running the PHPUnit Code Coverage

To run the code coverage on the individual file, I would call the command like so:

./vendor/bin/phpunit --coverage-text tests/AppBundle/Entity/ClientTest.php

The above command executes the “phpunit” binary and then passes the “–coverage-text” option and then specifies the path to the unit test case file (called ClientTest.php). Below is an example of what the screenshot looks like when run.

PHPunit_code_coverage

As you can see above it shows you it ran 2 tests and 8 assertions and those tests passes (they are green to indicate passing). There is a timestamp showing when the report was run, and is followed by a summary.

If you take a look closely at the Summary section, this has important details regarding code coverage. For example, it shows Classes at 0.00% coverage. You may wonder why it is only 0.00% which doesn’t seem right.

According to PHPUnit’s documentation, class coverage is only complete when “all of its methods are covered”. Notice on the bottom of the screenshot, under the “\AppBundle\Entity::Client” area, it shows in yellow, that only 6/7 Methods are covered.

The problem was that I was not testing all methods of the Client class, and I needed to add the “getClientId()” method. The client test code now looks like the following:

public function testCreateClient(){
   $client = new Client(); // Create a Client object.
   $client->setUserAgent("My Browser Type");
   $client->setClientIP("192.168.100.1");
   $now = new \DateTime("now");
   $client->setTimeSubmit( $now );

   // Verify settings.
   $this->assertEquals("My Browser Type", $client->getUserAgent());
   $this->assertEquals("192.168.100.1", $client->getClientIP());
   $this->assertEquals($now, $client->getTimeSubmit());
   $this->assertLessThan(3, $client->getClientId()); // Test 7th Method.
}

Now if we run the phpunit test with code coverage, we get a different result:

PHPunit_code_coverage2

This is better. Now in the Summary area we have “1/30” Classes covered. In otherwords, we need to develop test cases for 29 other Classes. If we look at the bottom for the Methods of the Client Entity, all 7 methods have been tested (100% coverage).

NOTE: Event though this shows 100% coverage, this does not mean that all scenarios of testing the Class have been covered. For example, it’s also a good idea to perform negative tests.

Hopefully this helps someone out!

Simple PHPUnit Functional Tests in Symfony

Introduction

For a TRIO project I’m working on for Taft College, I needed to run some PHPUnit Functional tests for the TRIO Forms system I’m developing. Testing in Symfony is well documented. In this article, I will show is simple Functional test example.

Running Tests with PHPUnit Phar

If you try to run phpunit directly from a phar in your Symfony project, you most likely will get the following error:

PHP Fatal error:  Undefined class constant 'PARSE_CONSTANT' in
vendor/symfony/symfony/src/Symfony/Component/DependencyInjection/
Loader/YamlFileLoader.php on line 396

This error occurs from a PHPUnit namespace issue and is described in more detail in the following article.

The workaround is to instead use Symfony’s vendor binaries, which will be described further down in this article.

Sample code

I created the following simple code will will crawl my application’s homepage, check for a certain string on the page, and also look for the text of a link on the page:

<?php
// tests/AppBundle/Functional/DefaultControllerTest.php
namespace tests\AppBundle\Functional;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class DefaultControllerTest extends WebTestCase
{
   public function testShowHomepage()
   {
      $client = static::createClient( array(), array(
         'HTTP_HOST' => 'trio2', // Set HOST HTTP Header.
         'HTTP_USER_AGENT' => 'Symfony Browser/1.0', // Set Agent header.
      ));

      $crawler = $client->request('GET', '/'); // Gets crawler for homepage.
      $appLink = $crawler->selectLink('TRIO Application'); // Check for link.
      var_dump( $appLink->text() ); // Dump the link text for debugging.

      // Make sure this text appears on the page.
      $this->assertGreaterThan(
         0,
         $crawler->filter('html:contains("TRIO-SSS Application & Eligibility")')->count()
      );

      // Verify that the link text is "TRIO Application".
      $this->assertcontains(
         'TRIO Application',
         $appLink->text() // Get link text from crawler object.
      );
   }
}

Notice on Line 18, I do a var_dump() of the crawler text, this is just a debugging line so that I can see the text is what I’m trying to assert later.

Running the Test

Instead of running PHPUnit from a phar, we can run from Symfony’s binaries like so:

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

Where “./vendor/bin/phpunit” is the path to the phpunit binary. And in this case I’ve specified to run just the one test case by specifying the folder path and filename with “tests/AppBundle/Functional/DefaultControllerTest.php”.

The output will look like the following screenshot:

phpunit_1strun

There are a number of options you can specify when running phpunit. To see what these options are, just use this command:

./vendor/bin/phpunit --help

Hope this helps you out!

Basic Symfony phpunit Functional Test

Introduction

This article is about creating a simple phpunit test in Symfony. The reason you would want to run phpunit tests, is to verify that code changes you made still work. For example, if you query a home page, you are going to “expect” certain things to be on that page and when you make development code changes, when you run your phpunit test, you should get the same results. In other words the tests should pass (unless you are expecting failures).

Symfony Documentation

Symfony has fairly good documentation on how to get started writing phpunit tests on their Testing page. This is a good place to start reading to get familiar with the Goutte crawler and some basic testing.

A Simple Test

The following is some sample code that I used to run a simple phpunit test:

<?php
namespace Tests\AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class DefaultControllerTest extends WebTestCase{
    public function testIndex(){
       $client = static::createClient(); // Create client.
       $crawler = $client->request('GET', '/'); // Get default content.

      // The HTML response code is 200.
      $this->assertEquals(200, $client->getResponse()->getStatusCode());

      // Asserts contains the specified text.
      $this->assertContains(
              'Application System',
              $client->getResponse()->getContent()
      );

      // Assert that there are exactly 1 h2 tags on the page
      $this->assertCount(1, $crawler->filter('h1'));

      // Asserts there are greater than 2 "<a>" tags - will fail.
      $this->assertGreaterThan(
          2,
          $crawler->filter('a')->count()
      );
   }
}

The above code simply gets the path “/”, which is the main page, then asserts the following:

  1. HTML response code is “200”.
  2. The page contains the text “Application System”.
  3. There is an exact count of 1 “h1” HTML tag.
  4. There are greater than 2 “a” HTML link tags.

For item 4, I am expecting that to fail, because I know that in my case I have exactly 2 “a” link tags on the page.

Running the Tests

To run the phpunit tests, from the Symfony project root folder run the following command:

phpunit

The tests will run and you should see a response similar to the following:

PHPunit_test

Notice the failure at step 4 as I expected. The PHPUnit version is also shown on the first output line.

This gives you an idea how to run simple phpunit tests in Symfony. Hopefully this helps you out.

Using Mink to Perform Functional Tests in Symfony3 Framework

Introduction

I was struggling to use the Symfony3 Goutte Web Scraper to create PHPUnit Functional tests; this is mainly because the Goutte Web Scraper is a headless browser and can’t handle JavaScript or AJAX web pages. In my application I had a few JavaScript functions on certain pages, so the tests had to be done in a browser. I’ve used Selenium a lot before, but not in PHPUnit.

I did a lot of searching online to see what I could find, and thanks to Benjamin Eberlei‘s on Using Mink in PHPUnit and also Andrew Chan‘s Set up and run Selenium on CentOS articles, I was able to figure out how to get the Selenium2Driver working with Mink.

 

Installing & Setup

To install Mink in your Symfony3 project, use the following command:

composer require behat/mink

You’ll need to install the Selenium2Driver as well:

composer require behat/mink-selenium2-driver

Installing PHPUnit might depend on what OS you are using, so just follow the installation instructions. Make sure you run –self-update to get the latest version.

You’ll need the latest Java version. How you install, will depend on the OS you are using, but I happen to have CentOS:

yum install java-1.8.0-openjdk-devel.i686

Since CentOS is a server OS, it also doesn’t have X Window System installed nor does it have a FireFox browser, and you’ll need this to do Selenium2Driver tests. So install FireFox and X with the following commands (this can take a long time):

yum -y install firefox Xvfb libXfont Xorg
yum -y groupinstall "X Window System" "Desktop" "Fonts" "General Purpose Desktop"

You’ll also need to download the selenium-server-standalone JAR file. I used the version 2.53 JAR file here: https://selenium-release.storage.googleapis.com/2.53/selenium-server-standalone-2.53.1.jar
Important: You need to use version 2.53 or older versions of the JAR file, since newer versions of the JAR may not work with the version of FireFox installed.

After installing X Window System, you’ll need access to a direct console (preferably as an admin) and then run “startx” to bring up X and the GUI. Open up one terminal window and run the following command:

nohup java -jar selenium-server-standalone-2.53.0.jar

Open a second terminal window and enter the following command to launch a X Windows Virtual Frame Buffer:

Xvfb :99 -ac -screen 0 1280x1024x24

Then in a third terminal we’ll need to export the display:

export DISPLAY=:99

Once you’ve done that, keep the third terminal open (and all the other ones too), and you are set to run some Mink tests.

 

Create a Simple PHPUnit trait File

Using similar code to what Benjamin Eberlei mentioned in his article, I created the following simple PHPUnit trait file:

<?php

namespace Tests\AppBundle\Functional;

trait MinkSetup
{
	private $minkBaseUrl;
	private $minkSession;

	/**
	 * @before
	 */
	public function setupMinkSession()
	{
		$this->minkBaseUrl = 'http://192.168.0.2/app_dev.php';
		//$this->minkBaseUrl = 'http://192.168.0.2'

		$driver = new \Behat\Mink\Driver\Selenium2Driver('firefox');
		$this->minkSession = new \Behat\Mink\Session($driver);
		$this->minkSession->start();
	}

	public function getCurrentPage()
	{
		return $this->minkSession->getPage();
	}

	public function getCurrentPageContent()
	{
		return $this->getCurrentPage()->getContent();
	}

	public function visit($url)
	{
		$this->minkSession->visit($this->minkBaseUrl . $url);
	}

	public function login($user, $pass){
		$this->minkSession->visit($this->minkBaseUrl . '/login');	// Login link.
		$page = $this->getCurrentPage();

		$page->fillField('username', $user);	// Enter username.
		$page->fillField('password', $pass);	// Enter password.
		$page->pressButton('_submit');

		$content = $this->getCurrentPageContent();
		$this->assertContains('logout', $content);	// Check that 'logout' exists.
	}

	/**
	 * @afterClass
	 */
	public function logout(){
		$page = $this->getCurrentPage();
		$page->clickLink('logout');
	}
}

The differences between Benjamin’s file and mine are:

  • Uses Selenium2Driver
  • Mink base URL is set directly in the trait file, not the PHPUnit dist xml file.
  • I added a login() function, which I use in all my tests to authenticate.
  • There is also a @afterClass annotation logout() method that gets called after each test.

Then after we create this file, we can use it in any of our functional test case files.

 

Creating a Simple Functional Test Case

The following code uses the above trait file and has a simple function to login and get some data and make assertions. It is a good idea at first to perform a very simple test to first verify that everything is working correctly.

<?php

namespace Tests\AppBundle\Functional;

class MinkPetitionTest extends \PHPUnit_Framework_TestCase
{
	use MinkSetup;

	public function testSubmitPage(){
		$this->login('some_user', 'myPassword');	// Login first.

		$this->visit('/submitPetStuSearch');	// Go to submit search
		$page = $this->getCurrentPage();	// Get the page.
		$page->fillField('form_ban_id', '1234');
		$page->pressButton('form_find_student');

		$content = $this->getCurrentPageContent();	// Get page content.
		$this->assertContains('<u>No Petitions</u> exist for Some User Student ID: 1234', $content);
	}
}

Once you’ve run the simple test case, then you can continue to modify your file and add more test cases/assertions as needed.

 

Element not clickable at point error in Mink Functional Testing

Introduction

Mink is a software tool that you can use with various PHP frameworks to perform functional or unit tests of your particular system/application. It simulates either a headless or a browser emulator or a browser controller (like Selenium). I started using it because, my application uses JavaScript and the headless Goutte PHP Web Scraper which is part of the Symfony framework cannot handle JavaScript functions. Although – I really like the Goutte driver.

When running some of my tests, I ran into the error “Element is not clickable at point …”, which is quite a common problem in Selenium testing. But I actually just found out why I got that error, and wanted to document my finding to help someone out who might run into the same problem.

Here is a cut & paste of the exact error, and looking at it now, it makes more sense (see below) the way the error message is worded:

WebDriver\Exception\UnknownError: Element is not clickable at point (85, 689.86669921875). Other element would receive the click:
<div class="sf-toolbar-icon"></div>

 

My Sample Code

Here is some of the test code I was running, and I’ll mark the line where the error occurred

$this->visit('/submitPet/6'); // Go directly to submit pet.
$page = $this->getCurrentPage();
$page->fillField('form_pet_course', 'HIST 231');
$page->selectFieldOption('form_pet_division', 'Social Science', false);
$page->selectFieldOption('form_pet_grade', 'B+', false);
$page->fillField('form_cor_units', '4.00');
$page->selectFieldOption('form_school_name', '2', false); // Set to 'BC'.
$page->selectFieldOption('form_during_sem', 'fall', false);
$page->selectFieldOption('form_sem_quarter', 'semester', false);
$page->fillField('form_taken_year', '2014');
$page->fillField('form_sub_requirement', 'TC American History &amp; Institutions');
$page->checkField('form_cal_ID');
$page->fillField('form_cal_ID_text', 'HIST 130');
$page->checkField('form_c2c_equiv');
$page->fillField('form_c2c_equiv_text', 'course2course_text');
$page->checkField('form_pass_along');
$page->fillField('form_pass_along_text', 'passAlong_text');
$page->checkField('form_maj_req_course');
$page->fillField('form_maj_req_course_text', 'HIST 2231');
$page->checkField('form_elect_course');
$page->fillField('form_elect_course_text', 'elect_course');
$page->checkField('form_comp_reading'); // <--- Got the error right here!!!

 

How I found out the Problem

It always failed at line 172 of my source file, which was annoying, because I knew the code was correct and everything looked like it should work. I double checked many times. So then I realized Mink (and Selenium) has a screenshot function. So I added in a screenshot on the line before the failure, just like so:

 

$page->fillField('form_elect_course_text', 'elect_course');
file_put_contents('/var/www/html/petition/web/sample_data/Fail.png',
                  $this->minkSession->getDriver()->getScreenshot());
$page->checkField('form_comp_reading');

This created a screenshot in that particular web folder and I took a look at it and this is what it looks like:

Fail

Do you see the problem? The debug toolbar is hovering over the middle of the page, and you can’t click on anything there. So the error message is correct, but I never realized why until I saw the screenshot.

Workarounds

Two work arounds are possible:

  • Close the debug toolbar when doing testing.
  • Test in the PROD environment.

Closing the debug bar is as simple as using Mink to find the element by CSS class for the Symfony debug toolbar, and then clicking on it:

 

$closeDebug = $page->find('css', '.hide-button');
$closeDebug->click();

 

Then to use the PROD environment, don’t point your Mink base URL to append app_dev.php.

Closing

Hopefully this helps someone in the future if they see this kind of error.