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:
- The first argument is the model class name
- The second one is a
Criteria
object or a simple array of conditions - The third one can be:
true
to check that some objects match the conditionsfalse
to check that no object matches the conditions- or an integer to check the number of matching objects
Extend or create a tester
Using testers have severals advantages:
- Isolation: Thanks to the decoupling of the testers, we provide many more testing methods than before.
- Readability: Your tests are much more readable, thanks to the block concept and shorter method names.
- Extensibility: You can extend each tester with your very own methods or create your own tester class.
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:
$this->browser
: The current browser object$this->tester
: Thelime_test
object
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:
initialize()
: This method is called every time you are usingwith()
in your tests. This is useful to get some object after the request has been sent:public function initialize() { $this->request = $this->browser->getRequest(); }
prepare()
: This method is called just before any call to the browser object. This is useful if you need to do something just before the request is send.
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 ;
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 ;
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.
how a nice tutorial!, since i didn't know so much about functional tests, this post and some previous tutorials are great to understand more on how symfony handles this.
I insist , is there any posibility to move this great posts to another place like a wiki where many users can contribure in an easy way??? I think this will help to document some things like this from well experimented symfony users... Thanks for keep us updated about changes in sf 1.2!!
I love the new Testers and especially the new info() and debug() methods. :)
Thanks for the effort in revamping the testing with symfony. It was great before, it's even more awesome now.
Nice to see, that the my ideas from ticket #4305 made it into sf1.2 - that's what I love about open source!
Regarding the ticket: Was the use of the linux cli programm 'highlight' to heavy in your opinion? I really like the highlighted html output, because it's so much more readable. I think we can do it directly in php as well - without external dependencies.
[http://trac.symfony-project.org/ticket/4305]
At last a short way to test my session variables. That new methods are awesome. Could be nice if the highlight would be independent from the OS, (I used windows in my job bright now and I missed a lot linux).
As always great work!
I think, probably, I'm gonna making this comment more and more from now on:
Just.... wow.
I like the new debug function, i think it will be really usefull.