Blog

New in symfony 1.2: Test your applications

Symfony Live 2010 Paris Conference

« Back to the Blog

Categories

Feeds

feed Posts feed

comments feed Comments feed

symfony training
Be trained by symfony experts
Feb 15: Paris (What's new in 1.3 / 1.4 - English)
Feb 15: Paris (and Zend Framework Together - English)
Feb 15: Paris (Hosting Practices with symfony - English)
Feb 24: Paris (1.4 + Doctrine - Français)
Mar 04: Online (What's new in 1.3/1.4 - Français)
and more...

Archives

Creative Commons License This work is licensed under a Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 Unported License.

The symfony framework has always been bundled with a functional testing framework and it is certainly one of its main strengths.

What is a functional test? Functional tests goal is to test the integration of all your application layers: from the routing to the controller, templates, and database calls. They do not replace unit tests.

The only thing it cannot test easily is the JavaScript code embedded in your templates. You can of course use a tool like selenium for this. But the good news is that the functional test framework can test "some" JavaScript code like Ajax calls.

To do its job, the functional testing framework simulates a browser. It does not need a web server as it knows symfony internals and how to generate a response based on a request. This allows easy and deep introspection of the state of your application after each request. You can of course introspect the symfony core objects like the response or the user session, but also your own code like the model.

Each release of symfony makes the functional testing framework even better. Today, I will show you all the goodness we have added for the symfony 1.2 version. Be prepared to be amazed!

Decoupling

Everybody knows that I like testing very much. I also like to refactor old code to make it better. For symfony 1.2, I have refactored the browser (sfBrowser) and the test browser (sfTextBrowser) classes to make them much more flexible and configurable.

As of symfony 1.2, the functional testing framework is made of several distinct and reusable layers.

The biggest changes is the introduction of testers. Testers are objects that knows how to test a specific layer of your application. Symfony comes with several built-in testers for the request, the response, the user, the view cache, forms, and Propel.

A less important change is the introduction of the sfTestFunctional class, which relies on a sfBrowser object to test your application and manages all the registered testers.

Here is a typical functional test:

$browser = new sfTestFunctional(new sfBrowser());
 
$browser->
  get('/')->
  // do some tests
;
 

To retain backward compatibility with symfony 1.0 and 1.1, you can still use the now deprecated sfTestBrowser class:

$browser = new sfTestBrowser();
 
$browser->
  get('/')->
  // do some tests
;
 

Testers

So, all the testing is actually done by tester classes. A tester knows how to test a specific part of your application.

The testers replace all the methods you are used to, like checkResponseElement() or isRequestParameter(). Of course, these methods are still available to retain backward compatibility (the UPGRADE_TO_1_2 file contains a table that references all the old methods and their tester equivalent).

Here is a simple example that demonstrates how to replace isRequestParameter() calls by using the request tester:

// before symfony 1.2
$browser->
  get('/')->
 
  isRequestParameter('module', 'foo')->
  checkResponseElement('h1', 'foo')
;
 
// as of symfony 1.2
$browser->
  get('/')->
 
  with('request')->isParameter('module', 'foo')->
  checkResponseElement('h1', 'foo')
;
 

The with('request') call switches the context of the fluent interface to the request tester object for the very next call. So, the isParameter() method is a sfTesterRequest method.

You can also create a block of calls in which the context is the tester object:

$browser->
  get('/')->
 
  with('request')->begin()->
    isParameter('module', 'foo')->
    isParameter('action', 'index')->
  end()->
 
  checkResponseElement('h1', 'foo')
;
 

All the method calls between begin() and end() are called against the current tester object.

Let's see the testing methods provided by the built-in tester classes.

Request Tester

The request tester is defined in the sfTesterRequest class and contains the following methods:

Method Description
isParameter Tests a request parameter
isMethod Tests the request method
isFormat Tests the request format
hasCookie Tests if the request has a given cookie
isCookie Tests the value of a cookie
$browser->
  get('/')->
 
  with('request')->begin()->
    isParameter('module', 'foo')->
    isMethod('get')->
    isFormat('html')->
    hasCookie('foo')->
    isCookie('foo', 'bar')->
  end()
;
 

Response Tester

The response tester is defined in the sfTesterResponse class and contains the following methods:

Method Description
isStatusCode Tests the response status code
contains Tests the response content with a simple regular expression
isHeader Tests the value of a given header
checkElement Checks the value of a CSS3 selector
$browser->
  get('/')->
 
  with('response')->begin()->
    isStatusCode(200)->
    contains('foo')->
    isHeader('Content-Type', 'text/plain')->
    checkElement('ul.foo li:last', '/foo/')->
  end()
;
 

View cache tester

The view cache tester is defined in the sfTesterViewCache class and contains the following methods:

Method Description
isCached Checks if a page/action is in the cache
isUriCached Checks if a specific URI (can be a partial) is in cache
$browser->
  get('/')->
 
  with('view_cache')->begin()->
    isCached(true)->
    isUriCached('@sf_cache_partial?module=foo&action=_partial&sf_cache_key=some_cache_key')->
  end()
;
 

User tester

The user tester is defined in the sfTesterUser class and contains the following methods:

Method Description
isCulture Tests the culture of the user
isAuthenticated Checks that the user is authenticated
hasCredential Checks for a user credential
isAttribute Tests the value of a given attribute
isFlash Tests the value of a flash variable
$browser->
  get('/')->
 
  with('user')->begin()->
    isCulture('fr')->
    isAuthenticated(true)->
    hasCredential('admin')->
    isAttribute('sfguard_user_id', '3')->
    isFlash('notice', '/foo/')->
  end()
;
 

Form Tester

Time to discover some new sexy testers!

The form tester is defined in sfTesterForm class. It knows if a form has been used in the previous request, has a reference to the form object itself, and allows you to introspect it.

Method Description
hasErrors Checks if the submitted form has some error
isError Tests the value of an error for a given field
hasGlobalError Same as isError but for global errors

The isError() method takes the same kind of second argument as the checkResponseElement() method.

$browser->
  click('save', array(...))->
  with('form')->begin()->
    hasErrors()->
    hasGlobalError('The login and password does not match.')->
    isError('name', 'Required.')->
    isError('name', '/Required/')->
    isError('name', '!/Invalid/')->
    isError('name')->
    isError('name', false)->
    isError('name', 1)->
  end()
;
 

Propel Tester

Here is another great tester: the propel tester.

It does not replace HTML response checks but is a mean to also check things that are not displayed in the browser but nonetheless important to test (for example if the last_connection timestamp for a user has been updated, or if the number of views for an article has been incremented, ...).

The propel tester is defined in sfTesterPropel in the Propel plugin and must be registered before being used:

$browser->setTester('propel', 'sfTesterPropel');
 

After the tester is registered, you can use it in your tests:

$browser->
  post('/')->
  with('propel')->begin()->
    check('Article', array('title' => 'foo'), false)->
    check('Article', array('title' => '!foo'), false)->
    check('Article', array(), 4)->
    check('Article', array('title' => '%foo%'), true)->
    check('Article', array('title' => '!%foo%'))->
    check('Article', $criteria)->
  end()
;
 

The propel tester only provides one method: check(). This method behaves differently based on the arguments you pass to it:

Extend or create a tester

Using testers have severals advantages:

Extend a built-in tester

If you want to add some methods to an existing tester, you need to create a class that inherits from the built-in tester and re-register it with your own class name:

class ApplicationTesterRequest extends sfTesterRequest
{
  // add some tester methods
}
 
// in your functional tests
$browser->setTester('request', 'ApplicationTesterRequest');
 

If you need to override a bunch of built-in testers, you can use the setTesters method:

$browser->setTesters(array(
  'request'  => 'ApplicationTesterRequest',
  'response' => 'ApplicationTesterResponse',
));
 

A tester method can do anything you like but must always end with the following code for the fluent interface to work correctly:

return $this->getObjectToReturn();
 

In your method, you have access to several objects:

Create a new tester

You can also create new tester class by registering it with a unique name:

$browser->setTester('my_tester', 'myTester');
 

A tester class inherits from sfTester and must implement the following methods:

Be fluent

When you write a lot of functional tests for a given module, it is sometimes useful to have some visual information about what it is being done. The new testers adds a new level of indentation and make tests more readable.

Also, there is a new info() method that outputs some text to help categorize your tests:

$browser->
  info('First scenario: Form with errors')->
  // ... some tests
  info('Second scenario: Valid form submission')->
  // ... some more tests
;
 

info in the browser

Debugging tests

When a problem occurs in a functional test, the HTML transfered to the browser help diagnose the cause. As of symfony 1.2, this is quite easy to display the generated HTML without interrupting the fluent interface style:

$browser->
  get('/a_uri_with_an_error')->
  with('response')->debug()->
  // some tests that won't be executed
;
 

The debug() method will output the response headers and content and will interrupt the flow of the browser.

The same debug() method exists for the form tester and outputs the submitted values and the form errors if any:

$browser->
  post('/post_to_a_form_with_some_errors')->
  with('form')->debug()->
  // some tests that won't be executed
;
 

debug

That's all for today. It has never been easier to test your symfony applications. So, I hope the new testing framework will convince you that it is not that hard and that it can save your day.

As for the new web debug toolbar panels, if you create new testers, don't hesitate to package them as a plugin.

Comments comments feed

The Sensio Labs Network

Since 1998, Sensio Labs has been promoting the Open-Source software movement by providing quality web application development, training, consulting.
Sensio Labs also supports several large Open-Source projects.