[ Index ]
 

Code source de Symfony 1.0.0

Accédez au Source d'autres logiciels libresSoutenez Angelica Josefina !

title

Body

[fermer]

/doc/ -> 15-Unit-and-Functional-Testing.txt (source)

   1  Chapter 15 - Unit And Functional Testing
   2  ========================================
   3  
   4  Automated tests are one of the greatest advances in programming since object orientation. Particularly conducive to developing web applications, they can guarantee the quality of an application even if releases are numerous. Symfony provides a variety of tools for facilitating automated testing, and these are introduced in this chapter.
   5  
   6  Automated Tests
   7  ---------------
   8  
   9  Any developer with experience developing web applications is well aware of the time it takes to do testing well. Writing test cases, running them, and analyzing the results is a tedious job. In addition, the requirements of web applications tend to change constantly, which leads to an ongoing stream of releases and a continuing need for code refactoring. In this context, new errors are likely to regularly crop up.
  10  
  11  That's why automated tests are a suggested, if not required, part of a successful development environment. A set of test cases can guarantee that an application actually does what it is supposed to do. Even if the internals are often reworked, the automated tests prevent accidental regressions. Additionally, they compel developers to write tests in a standardized, rigid format capable of being understood by a testing framework.
  12  
  13  Automated tests can sometimes replace developer documentation since they can clearly illustrate what an application is supposed to do. A good test suite shows what output should be expected for a set of test inputs, and that is a good way to explain the purpose of a method.
  14  
  15  The symfony framework applies this principle to itself. The internals of the framework are validated by automated tests. These unit and functional tests are not bundled with the standard symfony distribution, but you can check them out from the SVN repository or browse them online at [http://www.symfony-project.com/trac/browser/trunk/test](http://www.symfony-project.com/trac/browser/trunk/test).
  16  
  17  ### Unit and Functional Tests
  18  
  19  Unit tests confirm that a unitary code component provides the correct output for a given input. They validate how functions and methods work in every particular case. Unit tests deal with one case at a time, so for instance a single method may need several unit tests if it works differently in certain situations.
  20  
  21  Functional tests validate not a simple input-to-output conversion, but a complete feature. For instance, a cache system can only be validated by a functional test, because it involves more than one step: The first time a page is requested, it is rendered; the second time, it is taken from the cache. So functional tests validate a process and require a scenario. In symfony, you should write functional tests for all your actions.
  22  
  23  For the most complex interactions, these two types may fall short. Ajax interactions, for instance, require a web browser to execute JavaScript, so automatically testing them requires a special third-party tool. Furthermore, visual effects can only be validated by a human.
  24  
  25  If you have an extensive approach to automated testing, you will probably need to use a combination of all these methods. As a guideline, remember to keep tests simple and readable.
  26  
  27  >**NOTE**
  28  >Automated tests work by comparing a result with an expected output. In other words, they evaluate assertions (expressions like `$a == 2`). The value of an assertion is either `true` or `false`, and it determines whether a test passes or fails. The word "assertion" is commonly used when dealing with automated testing techniques.
  29  
  30  ### Test-Driven Development
  31  
  32  In the test-driven development (TDD) methodology, the tests are written before the code. Writing tests first helps you to focus on the tasks a function should accomplish before actually developing it. It's a good practice that other methodologies, like Extreme Programming (XP), recommend as well. Plus it takes into account the undeniable fact that if you don't write unit tests first, you never write them.
  33  
  34  For instance, imagine that you must develop a text-stripping function. The function removes white spaces at the beginning and at the end of the string, replaces nonalphabetical characters by underscores, and transforms all uppercase characters to lowercase ones. In test-driven development, you would first think about all the possible cases and provide an example "nput and expected output for each, as shown in Table 15-1.
  35  
  36  Table 15-1 - A List of Test Cases for a Text-Stripping Function
  37  
  38  Input                 | Expected Output
  39  --------------------- | ---------------------
  40  `" foo "`             | `"foo"`
  41  `"foo bar"`           | `"foo_bar"`
  42  `"-)foo:..=bar?"`     | `"__foo____bar_"`
  43  `"FooBar"`            | `"foobar`"
  44  `"Don't foo-bar me!"` | `"don_t_foo_bar_me_"`
  45  
  46  You would write the unit tests, run them, and see that they fail. You would then add the necessary code to handle the first test case, run the tests again, see that the first one passes, and go on like that. Eventually, when all the test cases pass, the function is correct.
  47  
  48  An application built with a test-driven methodology ends up with roughly as much test code as actual code. As you don't want to spend time debugging your tests cases, keep them simple.
  49  
  50  >**NOTE**
  51  >Refactoring a method can create new bugs that didn't use to appear before. That's why it is also a good practice to run all automated tests before deploying a new release of an application in production--this is called regression testing.
  52  
  53  ### The Lime Testing Framework
  54  
  55  There are many unit test frameworks in the PHP world, with the most well known being PhpUnit and SimpleTest. Symfony has its own, called lime. It is based on the `Test::More` Perl library, and is TAP compliant, which means that the result of tests is displayed as specified in the Test Anything Protocol, designed for better readability of test output.
  56  
  57  Lime provides support for unit testing. It is more lightweight than other PHP testing frameworks and has several advantages:
  58  
  59    * It launches test files in a sandbox to avoid strange side effects between each test run. Not all testing frameworks guarantee a clean environment for each test.
  60    * Lime tests are very readable, and so is the test output. On compatible systems, lime uses color output in a smart way to distinguish important information.
  61    * Symfony itself uses lime tests for regression testing, so many examples of unit and functional tests can be found in the symfony source code.
  62    * The lime core is validated by unit tests.
  63    * It is written in PHP, and it is fast and well coded. It is contained in a single file, `lime.php`, without any dependence.
  64  
  65  The various tests described next use the lime syntax. They work out of the box with any symfony installation.
  66  
  67  >**NOTE**
  68  >Unit and functional tests are not supposed to be launched in production. They are developer tools, and as such, they should be run in the developer's computer, not in the host server.
  69  
  70  Unit Tests
  71  ----------
  72  
  73  Symfony unit tests are simple PHP files ending in `Test.php` and located in the `test/unit/` directory of your application. They follow a simple and readable syntax.
  74  
  75  ### What Do Unit Tests Look Like?
  76  
  77  Listing 15-1 shows a typical set of unit tests for the `strtolower()` function. It starts by an instantiation of the `lime_test` object (you don't need to worry about the parameters for now). Each unit test is a call to a method of the `lime_test` instance. The last parameter of these methods is always an optional string that serves as the output.
  78  
  79  Listing 15-1 - Example Unit Test File, in `test/unit/strtolowerTest.php`
  80  
  81      [php]
  82      <?php
  83  
  84      include(dirname(__FILE__).'/../bootstrap/unit.php');
  85      require_once(dirname(__FILE__).'/../../lib/strtolower.php');
  86  
  87      $t = new lime_test(7, new lime_output_color());
  88  
  89      // strtolower()
  90      $t->diag('strtolower()');
  91      $t->isa_ok(strtolower('Foo'), 'string',
  92          'strtolower() returns a string');
  93      $t->is(strtolower('FOO'), 'foo',
  94          'strtolower() transforms the input to lowercase');
  95      $t->is(strtolower('foo'), 'foo',
  96          'strtolower() leaves lowercase characters unchanged');
  97      $t->is(strtolower('12#?@~'), '12#?@~',
  98          'strtolower() leaves non alphabetical characters unchanged');
  99      $t->is(strtolower('FOO BAR'), 'foo bar',
 100          'strtolower() leaves blanks alone');
 101      $t->is(strtolower('FoO bAr'), 'foo bar',
 102          'strtolower() deals with mixed case input');
 103      $t->is(strtolower(''), 'foo',
 104          'strtolower() transforms empty strings into foo');
 105  
 106  Launch the test set from the command line with the `test-unit` task. The command-line output is very explicit, and it helps you localize which tests failed and which passed. See the output of the example test in Listing 15-2.
 107  
 108  Listing 15-2 - Launching a Single Unit Test from the Command Line
 109  
 110      > symfony test-unit strtolower
 111  
 112      1..7
 113      # strtolower()
 114      ok 1 - strtolower() returns a string
 115      ok 2 - strtolower() transforms the input to lowercase
 116      ok 3 - strtolower() leaves lowercase characters unchanged
 117      ok 4 - strtolower() leaves non alphabetical characters unchanged
 118      ok 5 - strtolower() leaves blanks alone
 119      ok 6 - strtolower() deals with mixed case input
 120      not ok 7 - strtolower() transforms empty strings into foo
 121      #     Failed test (.\batch\test.php at line 21)
 122      #            got: ''
 123      #       expected: 'foo'
 124      # Looks like you failed 1 tests of 7.
 125  
 126  >**TIP**
 127  >The `include` statement at the beginning of Listing 15-1 is optional, but it makes the test file an independent PHP script that you can execute without the symfony command line, by calling `php test/unit/strtolowerTest.php`.
 128  
 129  ### Unit Testing Methods
 130  
 131  The `lime_test` object comes with a large number of testing methods, as listed in Table 15-2.
 132  
 133  Table 15-2 - Methods of the `lime_test` Object for Unit Testing
 134  
 135  Method                                      | Description
 136  ------------------------------------------- | -------------------------------------------------------------
 137  `diag($msg)`                                | Outputs a comment but runs no test
 138  `ok($test, $msg)`                           | Tests a condition and passes if it is true
 139  `is($value1, $value2, $msg)`                | Compares two values and passes if they are equal (`==`)
 140  `isnt($value1, $value2, $msg)`              | Compares two values and passes if they are not equal
 141  `like($string, $regexp, $msg)`              | Tests a string against a regular expression
 142  `unlike($string, $regexp, $msg)`            | Checks that a string doesn't match a regular expression
 143  `cmp_ok($value1, $operator, $value2, $msg)` | Compares two arguments with an operator
 144  `isa_ok($variable, $type, $msg)`            | Checks the type of an argument
 145  `isa_ok($object, $class, $msg)`             | Checks the class of an object
 146  `can_ok($object, $method, $msg)`            | Checks the availability of a method for an object or a class
 147  `is_deeply($array1, $array2, $msg)`         | Checks that two arrays have the same values
 148  `include_ok($file, $msg)`                   | Validates that a file exists and that it is properly included
 149  `fail()`                                    | Always fails--useful for testing exceptions
 150  `pass()`                                    | Always passes--useful for testing exceptions
 151  `skip($msg, $nb_tests)`                     | Counts as `$nb_tests` tests--useful for conditional tests
 152  `todo()`                                    | Counts as a test--useful for tests yet to be written
 153  
 154  The syntax is quite straightforward; notice that most methods take a message as their last parameter. This message is displayed in the output when the test passes. Actually, the best way to learn these methods is to test them, so have a look at Listing 15-3, which uses them all.
 155  
 156  Listing 15-3 - Testing Methods of the `lime_test` Object, in `test/unit/exampleTest.php`
 157  
 158      [php]
 159      <?php
 160  
 161      include(dirname(__FILE__).'/../bootstrap/unit.php');
 162  
 163      // Stub objects and functions for test purposes
 164      class myObject
 165      {
 166        public function myMethod()
 167        {
 168        }
 169      }
 170  
 171      function throw_an_exception()
 172      {
 173        throw new Exception('exception thrown');
 174      }
 175  
 176      // Initialize the test object
 177      $t = new lime_test(16, new lime_output_color());
 178  
 179      $t->diag('hello world');
 180      $t->ok(1 == '1', 'the equal operator ignores type');
 181      $t->is(1, '1', 'a string is converted to a number for comparison');
 182      $t->isnt(0, 1, 'zero and one are not equal');
 183      $t->like('test01', '/test\d+/', 'test01 follows the test numbering pattern');
 184      $t->unlike('tests01', '/test\d+/', 'tests01 does not follow the pattern');
 185      $t->cmp_ok(1, '<', 2, 'one is inferior to two');
 186      $t->cmp_ok(1, '!==', true, 'one and true are not identical');
 187      $t->isa_ok('foobar', 'string', '\'foobar\' is a string');
 188      $t->isa_ok(new myObject(), 'myObject', 'new creates object of the right class');
 189      $t->can_ok(new myObject(), 'myMethod', 'objects of class myObject do have amyMethod method');
 190      $array1 = array(1, 2, array(1 => 'foo', 'a' => '4'));
 191      $t->is_deeply($array1, array(1, 2, array(1 => 'foo', 'a' => '4')),
 192          'the first and the second array are the same');
 193      $t->include_ok('./fooBar.php', 'the fooBar.php file was properly included');
 194  
 195      try
 196      {
 197        throw_an_exception();
 198        $t->fail('no code should be executed after throwing an exception');
 199      }
 200      catch (Exception $e)
 201      {
 202        $t->pass('exception catched successfully');
 203      }
 204  
 205      if (!isset($foobar))
 206      {
 207        $t->skip('skipping one test to keep the test count exact in the condition', 1);
 208      }
 209      else
 210      {
 211        $t->ok($foobar, 'foobar');
 212      }
 213  
 214      $t->todo('one test left to do');
 215  
 216  You will find a lot of other examples of the usage of these methods in the symfony unit tests.
 217  
 218  >**TIP**
 219  >You may wonder why you would use `is()` as opposed to `ok()` here. The error message output by `is()` is much more explicit; it shows both members of the test, while `ok()` just says that the condition failed.
 220  
 221  ### Testing Parameters
 222  
 223  The initialization of the `lime_test` object takes as its first parameter the number of tests that should be executed. If the number of tests finally executed differs from this number, the lime output warns you about it. For instance, the test set of Listing 15-3 outputs as Listing 15-4. The initialization stipulated that 16 tests were to run, but only 15 actually took place, so the output indicates this.
 224  
 225  Listing 15-4 - The Count of Test Run Helps You to Plan Tests
 226  
 227      > symfony test-unit example
 228  
 229      1..16
 230      # hello world
 231      ok 1 - the equal operator ignores type
 232      ok 2 - a string is converted to a number for comparison
 233      ok 3 - zero and one are not equal
 234      ok 4 - test01 follows the test numbering pattern
 235      ok 5 - tests01 does not follow the pattern
 236      ok 6 - one is inferior to two
 237      ok 7 - one and true are not identical
 238      ok 8 - 'foobar' is a string
 239      ok 9 - new creates object of the right class
 240      ok 10 - objects of class myObject do have a myMethod method
 241      ok 11 - the first and the second array are the same
 242      not ok 12 - the fooBar.php file was properly included
 243      #     Failed test (.\test\unit\testTest.php at line 27)
 244      #       Tried to include './fooBar.php'
 245      ok 13 - exception catched successfully
 246      ok 14 # SKIP skipping one test to keep the test count exact in the condition
 247      ok 15 # TODO one test left to do
 248      # Looks like you planned 16 tests but only ran 15.
 249      # Looks like you failed 1 tests of 16.
 250  
 251  The `diag()` method doesn't count as a test. Use it to show comments, so that your test output stays organized and legible. On the other hand, the `todo()` and `skip()` methods count as actual tests. A `pass()`/`fail()` combination inside a `try`/`catch` block counts as a single test.
 252  
 253  A well-planned test strategy must contain an expected number of tests. You will find it very useful to validate your own test files--especially in complex cases where tests are run inside conditions or exceptions. And if the test fails at some point, you will see it quickly because the final number of run tests won't match the number given during initialization.
 254  
 255  The second parameter of the constructor is an output object extending the `lime_output` class. Most of the time, as tests are meant to be run through a CLI, the output is a lime_output_color object, taking advantage of bash coloring when available.
 256  
 257  ### The test-unit Task
 258  
 259  The `test-unit` task, which launches unit tests from the command line, expects either a list of test names or a file pattern. See Listing 15-5 for details.
 260  
 261  Listing 15-5 - Launching Unit Tests
 262  
 263      // Test directory structure
 264      test/
 265        unit/
 266          myFunctionTest.php
 267          mySecondFunctionTest.php
 268          foo/
 269            barTest.php
 270  
 271      > symfony test-unit myFunction                   ## Run myFunctionTest.php
 272      > symfony test-unit myFunction mySecondFunction  ## Run both tests
 273      > symfony test-unit 'foo/*'                      ## Run barTest.php
 274      > symfony test-unit '*'                          ## Run all tests (recursive)
 275  
 276  ### Stubs, Fixtures, and Autoloading
 277  
 278  In a unit test, the autoloading feature is not active by default. Each class that you use in a test must be either defined in the test file or required as an external dependency. That's why many test files start with a group of `include` lines, as Listing 15-6 demonstrates.
 279  
 280  Listing 15-6 - Including Classes in Unit Tests
 281  
 282      [php]
 283      <?php
 284  
 285      include(dirname(__FILE__).'/../bootstrap/unit.php');
 286      include(dirname(__FILE__).'/../../config/config.php');
 287      require_once($sf_symfony_lib_dir.'/util/sfToolkit.class.php');
 288  
 289      $t = new lime_test(7, new lime_output_color());
 290  
 291      // isPathAbsolute()
 292      $t->diag('isPathAbsolute()');
 293      $t->is(sfToolkit::isPathAbsolute('/test'), true,
 294          'isPathAbsolute() returns true if path is absolute');
 295      $t->is(sfToolkit::isPathAbsolute('\\test'), true,
 296          'isPathAbsolute() returns true if path is absolute');
 297      $t->is(sfToolkit::isPathAbsolute('C:\\test'), true,
 298          'isPathAbsolute() returns true if path is absolute');
 299      $t->is(sfToolkit::isPathAbsolute('d:/test'), true,
 300          'isPathAbsolute() returns true if path is absolute');
 301      $t->is(sfToolkit::isPathAbsolute('test'), false,
 302          'isPathAbsolute() returns false if path is relative');
 303      $t->is(sfToolkit::isPathAbsolute('../test'), false,
 304          'isPathAbsolute() returns false if path is relative');
 305      $t->is(sfToolkit::isPathAbsolute('..\\test'), false,
 306          'isPathAbsolute() returns false if path is relative');
 307  
 308  In unit tests, you need to instantiate not only the object you're testing, but also the object it depends upon. Since unit tests must remain unitary, depending on other classes may make more than one test fail if one class is broken. In addition, setting up real objects can be expensive, both in terms of lines of code and execution time. Keep in mind that speed is crucial in unit testing because developers quickly tire of a slow process.
 309  
 310  Whenever you start including many scripts for a unit test, you may need a simple autoloading system. For this purpose, the `sfCore` class (which must be manually included) provides an `initSimpleAutoload()` method, which expects an absolute path as parameter. All the classes located under this path will be autoloaded. For instance, if you want to have all the classes located under `$sf_symfony_lib_dir/util/` autoloaded, start your unit test script as follows:
 311  
 312      [php]
 313      require_once($sf_symfony_lib_dir.'/util/sfCore.class.php');
 314      sfCore::initSimpleAutoload($sf_symfony_lib_dir.'/util');
 315  
 316  >**TIP**
 317  >The generated Propel objects rely on a long cascade of classes, so as soon as you want to test a Propel object, autoloading is necessary. Note that for Propel to work, you also need to include the files under the vendor/prop`el/` directory (so the call to `sfCore` becomes `sfCore::initSimpleAutoload(array(SF_ROOT_ DIR.'/lib/model', $sf_symfony_lib_dir.'/vendor/propel'));`) and to add the Propel core to the include path (by calling `set_include_path($sf_symfony_lib_dir.'/vendor'.PATH_SEPARATOR.SF_ROOT_DIR.PATH_SEPARATOR.get_include_path()`.
 318  
 319  Another good workaround for the autoloading issues is the use of stubs. A stub is an alternative implementation of a class where the real methods are replaced with simple canned data. It mimics the behavior of the real class, but without its cost. A good example of stubs is a database connection or a web service interface. In Listing 15-7, the unit tests for a mapping API rely on a `WebService` class. Instead of calling the real `fetch()` method of the actual web service class, the test uses a stub that returns test data.
 320  
 321  Listing 15-7 - Using Stubs in Unit Tests
 322  
 323      [php]
 324      require_once(dirname(__FILE__).'/../../lib/WebService.class.php');
 325      require_once(dirname(__FILE__).'/../../lib/MapAPI.class.php');
 326  
 327      class testWebService extends WebService
 328      {
 329        public static function fetch()
 330        {
 331          return file_get_contents(dirname(__FILE__).'/fixtures/data/fake_web_service.xml');
 332        }
 333      }
 334  
 335      $myMap = new MapAPI();
 336  
 337      $t = new lime_test(1, new lime_output_color());
 338  
 339      $t->is($myMap->getMapSize(testWebService::fetch(), 100));
 340  
 341  The test data can be more complex than a string or a call to a method. Complex test data is often referred to as fixtures. For coding clarity, it is often better to keep fixtures in separate files, especially if they are used by more than one unit test file. Also, don't forget that symfony can easily transform a YAML file into an array with the `sfYAML::load()` method. This means that instead of writing long PHP arrays, you can write your test data in a YAML file, as in Listing 15-8.
 342  
 343  Listing 15-8 - Using Fixture Files in Unit Tests
 344  
 345      [php]
 346      // In fixtures.yml:
 347      -
 348        input:   '/test'
 349        output:  true
 350        comment: isPathAbsolute() returns true if path is absolute
 351      -
 352        input:   '\\test'
 353        output:  true
 354        comment: isPathAbsolute() returns true if path is absolute
 355      -
 356        input:   'C:\\test'
 357        output:  true
 358        comment: isPathAbsolute() returns true if path is absolute
 359      -
 360        input:   'd:/test'
 361        output:  true
 362        comment: isPathAbsolute() returns true if path is absolute
 363      -
 364        input:   'test'
 365        output:  false
 366        comment: isPathAbsolute() returns false if path is relative
 367      -
 368        input:   '../test'
 369        output:  false
 370        comment: isPathAbsolute() returns false if path is relative
 371      -
 372        input:   '..\\test'
 373        output:  false
 374        comment: isPathAbsolute() returns false if path is relative
 375  
 376      // In testTest.php
 377      <?php
 378  
 379      include(dirname(__FILE__).'/../bootstrap/unit.php');
 380      include(dirname(__FILE__).'/../../config/config.php');
 381      require_once($sf_symfony_lib_dir.'/util/sfToolkit.class.php');
 382      require_once($sf_symfony_lib_dir.'/util/sfYaml.class.php');
 383  
 384      $testCases = sfYaml::load(dirname(__FILE__).'/fixtures.yml');
 385  
 386      $t = new lime_test(count($testCases), new lime_output_color());
 387  
 388      // isPathAbsolute()
 389      $t->diag('isPathAbsolute()');
 390      foreach ($testCases as $case)
 391      {
 392        $t->is(sfToolkit::isPathAbsolute($case['input']), $case['output'],$case['comment']);
 393      }
 394  
 395  Functional Tests
 396  ----------------
 397  
 398  Functional tests validate parts of your applications. They simulate a browsing session, make requests, and check elements in the response, just like you would do manually to validate that an action does what it's supposed to do. In functional tests, you run a scenario corresponding to a use case.
 399  
 400  ### What Do Functional Tests Look Like?
 401  
 402  You could run your functional tests with a text browser and a lot of regular expression assertions, but that would be a great waste of time. Symfony provides a special object, called `sfBrowser`, which acts like a browser connected to a symfony application without actually needing a server--and without the slowdown of the HTTP transport. It gives access to the core objects of each request (the request, session, context, and response objects). Symfony also provides an extension of this class called `sfTestBrowser`, designed especially for functional tests, which has all the abilities of the `sfBrowser` object plus some smart assert methods.
 403  
 404  A functional test traditionally starts with an initialization of a test browser object. This object makes a request to an action and verifies that some elements are present in the response.
 405  
 406  For example, every time you generate a module skeleton with the `init-module` or the `propel-init-crud` tasks, symfony creates a simple functional test for this module. The test makes a request to the default action of the module and checks the response status code, the module and action calculated by the routing system, and the presence of a certain sentence in the response content. For a `foobar` module, the generated `foobarActionsTest.php` file looks like Listing 15-9.
 407  
 408  Listing 15-9 - Default Functional Test for a New Module, in `tests/functional/frontend/foobarActionsTest.php`
 409  
 410      [php]
 411      <?php
 412  
 413      include(dirname(__FILE__).'/../../bootstrap/functional.php');
 414  
 415      // Create a new test browser
 416      $browser = new sfTestBrowser();
 417      $browser->initialize();
 418  
 419      $browser->
 420        get('/foobar/index')->
 421        isStatusCode(200)->
 422        isRequestParameter('module', 'foobar')->
 423        isRequestParameter('action', 'index')->
 424        checkResponseElement('body', '!/This is a temporary page/')
 425      ;
 426  
 427  >**TIP**
 428  >The browser methods return an `sfTestBrowser` object, so you can chain the method calls for more readability of your test files. This is called a fluid interface to the object, because nothing stops the flow of method calls.
 429  
 430  A functional test can contain several requests and more complex assertions; you will soon discover all the possibilities in the upcoming sections.
 431  
 432  To launch a functional test, use the `test-functional` task with the symfony command line, as shown in Listing 15-10. This task expects an application name and a test name (omit the `Test.php` suffix).
 433  
 434  Listing 15-10 - Launching a Single Functional Test from the Command Line
 435  
 436      > symfony test-functional frontend foobarActions
 437  
 438      # get /comment/index
 439      ok 1 - status code is 200
 440      ok 2 - request parameter module is foobar
 441      ok 3 - request parameter action is index
 442      not ok 4 - response selector body does not match regex /This is a temporary page/
 443      # Looks like you failed 1 tests of 4.
 444      1..4
 445  
 446  The generated functional tests for a new module don't pass by default. This is because in a newly created module, the `index` action forwards to a congratulations page (included in the symfony `default` module), which contains the sentence "This is a temporary page." As long as you don't modify the `index` action, the tests for this module will fail, and this guarantees that you cannot pass all tests with an unfinished module.
 447  
 448  >**NOTE**
 449  >In functional tests, the autoloading is activated, so you don't have to include the files by hand.
 450  
 451  ### Browsing with the sfTestBrowser Object
 452  
 453  The test browser is capable of making GET and POST requests. In both cases, use a real URI as parameter. Listing 15-11 shows how to write calls to the `sfTestBrowser` object to simulate requests.
 454  
 455  Listing 15-11 - Simulating Requests with the `sfTestBrowser` Object
 456  
 457      [php]
 458      include(dirname(__FILE__).'/../../bootstrap/functional.php');
 459  
 460      // Create a new test browser
 461      $b = new sfTestBrowser();
 462      $b->initialize();
 463  
 464      $b->get('/foobar/show/id/1');                   // GET request
 465      $b->post('/foobar/show', array('id' => 1));     // POST request
 466  
 467      // The get() and post() methods are shortcuts to the call() method
 468      $b->call('/foobar/show/id/1', 'get');
 469      $b->call('/foobar/show', 'post', array('id' => 1));
 470  
 471      // The call() method can simulate requests with any method
 472      $b->call('/foobar/show/id/1', 'head');
 473      $b->call('/foobar/add/id/1', 'put');
 474      $b->call('/foobar/delete/id/1', 'delete');
 475  
 476  A typical browsing session contains not only requests to specific actions, but also clicks on links and on browser buttons. As shown in Listing 15-12, the `sfTestBrowser` object is also capable of simulating those.
 477  
 478  Listing 15-12 - Simulating Navigation with the `sfTestBrowser` Object
 479  
 480      [php]
 481      $b->get('/');                  // Request to the home page
 482      $b->get('/foobar/show/id/1');
 483      $b->back();                    // Back to one page in history
 484      $b->forward();                 // Forward one page in history
 485      $b->reload();                  // Reload current page
 486      $b->click('go');               // Look for a 'go' link or button and click it
 487  
 488  The test browser handles a stack of calls, so the `back()` and `forward()` methods work as they do on a real browser.
 489  
 490  >**TIP**
 491  >The test browser has its own mechanisms to manage sessions (`sfTestStorage`) and cookies.
 492  
 493  Among the interactions that most need to be tested, those associated with forms probably rank first. To simulate form input and submission, you have three choices. You can either make a POST request with the parameters you wish to send, call `click()` with the form parameters as an array, or fill in the fields one by one and click the submit button. They all result in the same POST request anyhow. Listing 15-13 shows an example.
 494  
 495  Listing 15-13 - Simulating Form Input with the `sfTe stBrowser` Object
 496  
 497      [php]
 498      // Example template in modules/foobar/templates/editSuccess.php
 499      <?php echo form_tag('foobar/update') ?>
 500        <?php echo input_hidden_tag('id', $sf_params->get('id')) ?>
 501        <?php echo input_tag('name', 'foo') ?>
 502        <?php echo submit_tag('go') ?>
 503        <?php echo textarea('text1', 'foo') ?>
 504        <?php echo textarea('text2', 'bar') ?>
 505      </form>
 506  
 507      // Example functional test for this form
 508      $b = new sfTestBrowser();
 509      $b->initialize();
 510      $b->get('/foobar/edit/id/1');
 511  
 512      // Option 1: POST request
 513      $b->post('/foobar/update', array('id' => 1, 'name' => 'dummy', 'commit' => 'go'));
 514  
 515      // Option 2: Click the submit button with parameters
 516      $b->click('go', array('name' => 'dummy'));
 517  
 518      // Option 3: Enter the form values field by field name then click the submit button
 519      $b->setField('name', 'dummy')->
 520          click('go');
 521  
 522  >**NOTE**
 523  >With the second and third options, the default form values are automatically included in the form submission, and the form target doesn't need to be specified.
 524  
 525  When an action finishes by a `redirect()`, the test browser doesn't automatically follow the redirection; you must follow it manually with `followRedirect()`, as demonstrated in Listing 15-14.
 526  
 527  Listing 15-14 - The Test Browser Doesn't Automatically Follow Redirects
 528  
 529      [php]
 530      // Example action in modules/foobar/actions/actions.class.php
 531      public function executeUpdate()
 532      {
 533        ...
 534        $this->redirect('foobar/show?id='.$this->getRequestParameter('id'));
 535      }
 536  
 537      // Example functional test for this action
 538      $b = new sfTestBrowser();    
 539      $b->initialize();
 540      $b->get('/foobar/edit?id=1')->
 541          click('go', array('name' => 'dummy'))->
 542          isRedirected()->   // Check that request is redirected
 543          followRedirect();    // Manually follow the redirection
 544  
 545  There is one last method you should know about that is useful for browsing: `restart()` reinitializes the browsing history, session, and cookies--as if you restarted your browser.
 546  
 547  Once it has made a first request, the `sfTestBrowser` object can give access to the request, context, and response objects. It means that you can check a lot of things, ranging from the text content to the response headers, the request parameters, and configuration:
 548  
 549      [php]
 550      $request  = $b->getRequest();
 551      $context  = $b->getContext();
 552      $response = $b->getResponse();
 553  
 554  >**SIDEBAR**
 555  >The sfBrowser object
 556  >
 557  >All the browsing methods described in Listings 15-10 to 15-13 are also available out of the testing scope, throughout the `sfBrowser` object. You can call it as follows:
 558  >
 559  >     [php]
 560  >     // Create a new browser
 561  >     $b = new sfBrowser();
 562  >     $b->initialize();
 563  >     $b->get('/foobar/show/id/1')->
 564  >         setField('name', 'dummy')->
 565  >         click('go');
 566  >     $content = $b()->getResponse()->getContent();
 567  >     ...
 568  >
 569  >The `sfBrowser` object is a very useful tool for batch scripts, for instance, if you want to browse a list of pages to generate a cached version for each (refer to Chapter 18 for a detailed example).
 570  
 571  ### Using Assertions
 572  
 573  Due to the `sfTestBrowser` object having access to the response and other components of the request, you can do tests on these components. You could create a new `lime_test` object for that purpose, but fortunately `sfTestBrowser` proposes a `test()` method that returns a `lime_test` object where you can call the unit assertion methods described previously. Check Listing 15-15 to see how to do assertions via `sfTestBrowser`.
 574  
 575  Listing 15-15 - The Test Browser Provides Testing Abilities with the `test()` Method
 576  
 577      [php]
 578      $b = new sfTestBrowser();
 579      $b->initialize();
 580      $b->get('/foobar/edit/id/1');
 581      $request  = $b->getRequest();
 582      $context  = $b->getContext();
 583      $response = $b->getResponse();
 584  
 585      // Get access to the lime_test methods via the test() method
 586      $b->test()->is($request->getParameter('id'), 1);
 587      $b->test()->is($response->getStatuscode(), 200);
 588      $b->test()->is($response->getHttpHeader('content-type'), 'text/html;charset=utf-8');
 589      $b->test()->like($response->getContent(), '/edit/');
 590  
 591  >**NOTE**
 592  >The `getResponse()`, `getContext()`, `getRequest()`, and `test()` methods don't return an `sfTestBrowser` object, therefore you can't chain other `sfTestBrowser` method calls after them.
 593  
 594  You can check incoming and outgoing cookies easily via the request and response objects, as shown in Listing 15-16.
 595  
 596  Listing 15-16 - Testing Cookies with `sfTestBrowser`
 597  
 598      [php]
 599      $b->test()->is($request->getCookie('foo'), 'bar');     // Incoming cookie
 600      $cookies = $response->getCookies();
 601      $b->test()->is($cookies['foo'], 'foo=bar');            // Outgoing cookie
 602  
 603  Using the `test()` method to test the request elements ends up in long lines. Fortunately, `sfTestbrowser` contains a bunch of proxy methods that help you keep your functional tests readable and short--in addition to returning an `sfTestBrowser` object themselves. For instance, you can rewrite Listing 15-15 in a faster way, as shown in Listing 15-17.
 604  
 605  Listing 15-17 - Testing Directly with `sfTestBrowser`
 606  
 607      [php]
 608      $b = new sfTestBrowser();
 609      $b->initialize();
 610      $b->get('/foobar/edit/id/1')->
 611          isRequestParameter('id', 1)->
 612          isStatutsCode()->
 613          isResponseHeader('content-type', 'text/html; charset=utf-8')->
 614          responseContains('edit');
 615  
 616  The status 200 is the default value of the parameter expected by `isStatusCode()`, so you can call it without any argument to test a successful response.
 617  
 618  One more advantage of proxy methods is that you don't need to specify an output text as you would with a `lime_test` method. The messages are generated automatically by the proxy methods, and the test output is clear and readable.
 619  
 620      # get /foobar/edit/id/1
 621      ok 1 - request parameter "id" is "1"
 622      ok 2 - status code is "200"
 623      ok 3 - response header "content-type" is "text/html"
 624      ok 4 - response contains "edit"
 625      1..4
 626  
 627  In practice, the proxy methods of Listing 15-17 cover most of the usual tests, so you will seldom use the `test()` method on an `sfTestBrowser` object.
 628  
 629  Listing 15-14 showed that `sfTestBrowser` doesn't automatically follow redirections. This has one advantage: You can test a redirection. For instance, Listing 15-18 shows how to test the response of Listing 15-14.
 630  
 631  Listing 15-18 - Testing Redirections with `sfTestBrowser`
 632  
 633      [php]
 634      $b = new sfTestBrowser();
 635      $b->initialize();
 636      $b->
 637          get('/foobar/edit/id/1')->
 638          click('go', array('name' => 'dummy'))->
 639          isStatusCode(200)->
 640          isRequestParameter('module', 'foobar')->
 641          isRequestParameter('action', 'update')->
 642  
 643          isRedirected()->      // Check that the response is a redirect
 644          followRedirect()->    // Manually follow the redirection
 645  
 646          isStatusCode(200)->
 647          isRequestParameter('module', 'foobar')->
 648          isRequestParameter('action', 'show');
 649  
 650  ### Using CSS Selectors
 651  
 652  Many of the functional tests validate that a page is correct by checking for the presence of text in the content. With the help of regular expressions in the `responseContains()` method, you can check displayed text, a tag's attributes, or values. But as soon as you want to check something deeply buried in the response DOM, regular expressions are not ideal.
 653  
 654  That's why the `sfTestBrowser` object supports a `getResponseDom()` method. It returns a libXML2 DOM object, much easier to parse and test than a flat text. Refer to Listing 15-19 for an example of using this method.
 655  
 656  Listing 15-19 - The Test Browser Gives Access to the Response Content As a DOM Object
 657  
 658      [php]
 659      $b = new sfTestBrowser();
 660      $b->initialize();
 661      $b->get('/foobar/edit/id/1');
 662      $dom = $b->getResponseDom();
 663      $b->test()->is($dom->getElementsByTagName('input')->item(1)->getAttribute('type'),'text');
 664  
 665  But parsing an HTML document with the PHP DOM methods is still not fast and easy enough. If you are familiar with the CSS selectors, you know that they are an ever more powerful way to retrieve elements from an HTML document. Symfony provides a tool class called `sfDomCssSelector` that expects a DOM document as construction parameter. It has a getTexts() method that returns an array of strings according to a CSS selector, and a getElements() method that returns an array of DOM elements. See an example in Listing 15-20.
 666  
 667  Listing 15-20 - The Test Browser Gives Access to the Response Content As an `sfDomCssSelector` Object
 668  
 669      [php]
 670      $b = new sfTestBrowser();
 671      $b->initialize();
 672      $b->get('/foobar/edit/id/1');
 673      $c = new sfDomCssSelector($b->getResponseDom())
 674      $b->test()->is($c->getTexts('form input[type="hidden"][value="1"]'), array('');
 675      $b->test()->is($c->getTexts('form textarea[name="text1"]'), array('foo'));
 676      $b->test()->is($c->getTexts('form input[type="submit"]'), array(''));
 677  
 678  In its constant pursuit for brevity and clarity, symfony provides a shortcut for this: the `checkResponseElement()` proxy method. This method makes Listing 15-20 look like Listing 15-21.
 679  
 680  Listing 15-21 - The Test Browser Gives Access to the Elements of the Response by CSS Selectors
 681  
 682      [php]
 683      $b = new sfTestBrowser();
 684      $b->initialize();
 685      $b->get('/foobar/edit/id/1')->
 686          checkResponseElement('form input[type="hidden"][value="1"]', true->
 687          checkResponseElement('form textarea[name="text1"]', 'foo')->
 688          checkResponseElement('form input[type="submit"]', 1);
 689  
 690  The behavior of the `checkResponseElement()` method depends on the type of the second argument that it receives:
 691  
 692    * If it is a Boolean, it checks that an element matching the CSS selector exists.
 693    * If it is an integer, it checks that the CSS selector returns this number of results.
 694    * If it is a regular expression, it checks that the first element found by the CSS selector matches it.
 695    * If it is a regular expression preceded by `!`, it checks that the first element doesn't match the pattern.
 696    * For other cases, it compares the first element found by the CSS selector with the second argument as a string.
 697  
 698  The method accepts a third optional parameter, in the shape of an associative array. It allows you to have the test performed not on the first element returned by the selector (if it returns several), but on another element at a certain position, as shown in Listing 15-22.
 699  
 700  Listing 15-22 - Using the Position Option to Match an Element at a Certain Position
 701  
 702      [php]
 703      $b = new sfTestBrowser();
 704      $b->initialize();
 705      $b->get('/foobar/edit?id=1')->
 706          checkResponseElement('form textarea', 'foo')->
 707          checkResponseElement('form textarea', 'bar', array('position' => 1));
 708  
 709  The options array can also be used to perform two tests at the same time. You can test that there is an element matching a selector and how many there are, as demonstrated in Listing 15-23.
 710  
 711  Listing 15-23 - Using the Count Option to Count the Number of Matches
 712  
 713      [php]
 714      $b = new sfTestBrowser();
 715      $b->initialize();
 716      $b->get('/foobar/edit?id=1')->
 717          checkResponseElement('form input', true, array('count' => 3));
 718  
 719  The selector tool is very powerful. It accepts most of the CSS 2.1 selectors, and you can use it for complex queries such as those of Listing 15-24.
 720  
 721  Listing 15-24 - Example of Complex CSS Selectors Accepted by `checkResponseElement()`
 722  
 723      [php]
 724      $b->checkResponseElement('ul#list li a[href]', 'click me');
 725      $b->checkResponseElement('ul > li', 'click me');
 726      $b->checkResponseElement('ul + li', 'click me');
 727      $b->checkResponseElement('h1, h2', 'click me');
 728      $b->checkResponseElement('a[class$="foo"][href*="bar.html"]', 'my link');
 729  
 730  ### Working in the Test Environment
 731  
 732  The `sfTestBrowser` object uses a special front controller, set to the `test` environment. The default configuration for this environment appears in Listing 15-25.
 733  
 734  Listing 15-25 - Default Test Environment Configuration, in `myapp/config/settings.php`
 735  
 736      test:
 737        .settings:
 738          # E_ALL | E_STRICT & ~E_NOTICE = 2047
 739          error_reporting:        2047
 740          cache:                  off
 741          web_debug:              off
 742          no_script_name:         off
 743          etag:                   off
 744  
 745  The cache and the web debug toolbar are set to `off` in this environment. However, the code execution still leaves traces in a log file, distinct from the `dev` and `prod` log files, so that you can check it independently (`myproject/log/myapp_test.log`). In this environment, the exceptions don't stop the execution of the scripts--so that you can run an entire set of tests even if one fails. You can have specific database connection settings, for instance, to use another database with test data in it.
 746  
 747  Before using the `sfTestBrowser` object, you have to initialize it. If you need to, you can specify a hostname for the application and an IP address for the client--that is, if your application makes controls over these two parameters. Listing 15-26 demonstrates how to do this.
 748  
 749  Listing 15-26 - Setting Up the Test Browser with Hostname and IP
 750  
 751      [php]
 752      $b = new sfTestBrowser();
 753      $b->initialize('myapp.example.com', '123.456.789.123');
 754  
 755  ### The test-functional Task
 756  
 757  The `test-functional` task can run one or more functional tests, depending on the number of arguments received. The rules look much like the ones of the `test-unit` task, except that the functional test task always expects an application as first argument, as shown in Listing 15-27.
 758  
 759  Listing 15-27 - Functional Test Task Syntax
 760  
 761      // Test directory structure
 762      test/
 763        functional/
 764          frontend/
 765            myModuleActionsTest.php
 766            myScenarioTest.php
 767          backend/
 768            myOtherScenarioTest.php
 769  
 770      ## Run all functional tests for one application, recursively
 771      > symfony test-functional frontend
 772  
 773      ## Run one given functional test
 774      > symfony test-functional frontend myScenario
 775  
 776      ## Run several tests based on a pattern
 777      > symfony test-functional frontend my*
 778  
 779  Test Naming Practices
 780  ---------------------
 781  
 782  This section lists a few good practices to keep your tests organized and easy to maintain. The tips concern file organization, unit tests, and functional tests.
 783  
 784  As for the file structure, you should name the unit test files using the class they are supposed to test, and name the functional test files using the module or the scenario they are supposed to test. See Listing 15-28 for an example. Your `test/` directory will soon contain a lot of files, and finding a test might prove difficult in the long run if you don't follow these guidelines.
 785  
 786  Listing 15-28 - Example File Naming Practice
 787  
 788      test/
 789        unit/
 790          myFunctionTest.php
 791          mySecondFunctionTest.php
 792          foo/
 793            barTest.php
 794        functional/
 795          frontend/
 796            myModuleActionsTest.php
 797            myScenarioTest.php
 798          backend/
 799            myOtherScenarioTest.php
 800  
 801  For unit tests, a good practice is to group the tests by function or method, and start each test group with a `diag()` call. The messages of each unit test should contain the name of the function or method tested, followed by a verb and a property, so that the test output looks like a sentence describing a property of the object. Listing 15-29 shows an example.
 802  
 803  Listing 15-29 - Example Unit Test Naming Practice
 804  
 805      [php]
 806      // srttolower()
 807      $t->diag('strtolower()');
 808      $t->isa_ok(strtolower('Foo'), 'string', 'strtolower() returns a string');
 809      $t->is(strtolower('FOO'), 'foo', 'strtolower() transforms the input to lowercase');
 810  
 811      # strtolower()
 812      ok 1 - strtolower() returns a string
 813      ok 2 - strtolower() transforms the input to lowercase
 814  
 815  Functional tests should be grouped by page and start by a request. Listing 15-30 illustrates this practice.
 816  
 817  Listing 15-30 - Example Functional Test Naming Practice
 818  
 819      [php]
 820      $browser->
 821        get('/foobar/index')->
 822        isStatusCode(200)->
 823        isRequestParameter('module', 'foobar')->
 824        isRequestParameter('action', 'index')->
 825        checkResponseElement('body', '/foobar/')
 826      ;
 827  
 828      # get /comment/index
 829      ok 1 - status code is 200
 830      ok 2 - request parameter module is foobar
 831      ok 3 - request parameter action is index
 832      ok 4 - response selector body matches regex /foobar/
 833  
 834  If you follow this convention, the output of your test will be clean enough to use as a developer documentation of your project--enough so in some cases to make actual documentation useless.
 835  
 836  Special Testing Needs
 837  ---------------------
 838  
 839  The unit and functional test tools provided by symfony should suffice in most cases. A few additional techniques are listed here to resolve common problems in automated testing: launching tests in an isolated environment, accessing a database within tests, testing the cache, and testing interactions on the client side.
 840  
 841  ### Executing Tests in a Test Harness
 842  
 843  The `test-unit` and `test-functional` tasks can launch a single test or a set of tests. But if you call these tasks without any parameter, they launch all the unit and functional tests written in the `test/` directory. A particular mechanism is involved to isolate each test file in an independent sandbox, to avoid contamination risks between tests. Furthermore, as it wouldn't make sense to keep the same output as with single test files in that case (the output would be thousands of lines long), the tests results are compacted into a synthetic view. That's why the execution of a large number of test files uses a test harness, that is, an automated test framework with special abilities. A test harness relies on a component of the lime framework called `lime_harness`. It shows a test status file by file, and an overview at the end of the number of tests passed over the total, as you see in Listing 15-31.
 844  
 845  Listing 15-31 - Launching All Tests in a Test Harness
 846  
 847      > symfony test-unit
 848  
 849      unit/myFunctionTest.php................ok
 850      unit/mySecondFunctionTest.php..........ok
 851      unit/foo/barTest.php...................not ok
 852  
 853      Failed Test                     Stat  Total   Fail  List of Failed
 854      ------------------------------------------------------------------
 855      unit/foo/barTest.php               0      2      2  62 63
 856      Failed 1/3 test scripts, 66.66% okay. 2/53 subtests failed, 96.22% okay.
 857  
 858  The tests are executed the same way as when you call them one by one, only the output is made shorter to be really useful. In particular, the final chart focuses on the failed tests and helps you locate them.
 859  
 860  You can launch all the tests with one call using the `test-all` task, which also uses a test harness, as shown in Listing 15-32. This is something that you should do before every transfer to production, to ensure that no regression has appeared since the latest release.
 861  
 862  Listing 15-32 - Launching All the Tests of a Project
 863  
 864      > symfony test-all
 865  
 866  ### Accessing a Database
 867  
 868  Unit tests often need to access a database. A database connection is automatically initialized when you call `sfTestBrowser::get()` for the first time. However, if you want to access the database even before using `sfTestBrowser`, you have to initialize a `sfDabataseManager` object manually, as in Listing 15-33. 
 869  
 870  Listing 15-33 - Initializing a Database in a Test
 871  
 872      [php]
 873      $databaseManager = new sfDatabaseManager();
 874      $databaseManager->initialize();
 875      
 876      // Optionally, you can retrieve the current database connection
 877      $con = Propel::getConnection();
 878  
 879  You should populate the database with fixtures before starting the tests. This can be done via the `sfPropelData` object. This object can load data from a file, just like the `propel-load-data` task, or from an array, as shown in Listing 15-34.
 880  
 881  Listing 15-34 - Populating a Database from a Test File
 882  
 883      [php]
 884      $data = new sfPropelData();
 885  
 886      // Loading data from file
 887      $data->loadData(sfConfig::get('sf_data_dir').'/fixtures/test_data.yml');
 888  
 889      // Loading data from array
 890      $fixtures = array(
 891        'Article' => array(
 892          'article_1' => array(
 893            'title'      => 'foo title',
 894            'body'       => 'bar body',
 895            'created_at' => time(),
 896          ),
 897          'article_2'    => array(
 898            'title'      => 'foo foo title',
 899            'body'       => 'bar bar body',
 900            'created_at' => time(),
 901          ),
 902        ),
 903      );
 904      $data->loadDataFromArray($fixtures);
 905  
 906  Then, use the Propel objects as you would in a normal application, according to your testing needs. Remember to include their files in unit tests (you can use the sfCore:: sfSimpleAutoloading() method to automate it, as explained in a tip in the "Stubs, Fixtures, and Autoloading" section previously in this chapter). Propel objects are autoloaded in functional tests.
 907  
 908  ### Testing the Cache
 909  
 910  When you enable caching for an application, the functional tests should verify that the cached actions do work as expected.
 911  
 912  The first thing to do is enable cache for the test environment (in the `settings.yml` file). Then, if you want to test whether a page comes from the cache or whether it is generated, you should use the `isCached()` test method provided by the `sfTestBrowser` object. Listing 15-35 demonstrates this method.
 913  
 914  Listing 15-35 - Testing the Cache with the `isCached()` Method
 915  
 916      [php]
 917      <?php
 918  
 919      include(dirname(__FILE__).'/../../bootstrap/functional.php');
 920  
 921      // Create a new test browser
 922      $b = new sfTestBrowser();
 923      $b->initialize();
 924  
 925      $b->get('/mymodule');
 926      $b->isCached(true);       // Checks that the response comes from the cache
 927      $b->isCached(true, true); // Checks that the cached response comes with layout
 928      $b->isCached(false);      // Checks that the response doesn't come from the cache
 929  
 930  >**NOTE**
 931  >You don't need to clear the cache at the beginning of a functional test; the bootstrap script does it for you.
 932  
 933  ### Testing Interactions on the Client
 934  
 935  The main drawback of the techniques described previously is that they cannot simulate JavaScript. For very complex interactions, like with Ajax interactions for instance, you need to be able to reproduce exactly the mouse and keyboard input that a user would do and execute scripts on the client side. Usually, these tests are reproduced by hand, but they are very time consuming and prone to error.
 936  
 937  The solution is called Selenium ([http://www.openqa.org/selenium/](http://www.openqa.org/selenium/)), which is a test framework written entirely in JavaScript. It executes a set of actions on a page just like a regular user would, using the current browser window. The advantage over the `sfBrowser` object is that Selenium is capable of executing JavaScript in a page, so you can test even Ajax interactions with it.
 938  
 939  Selenium is not bundled with symfony by default. To install it, you need to create a new `selenium/` directory in your `web/` directory, and in it unpack the content of the Selenium archive ([http://www.openqa.org/selenium-core/download.action](http://www.openqa.org/selenium-core/download.action)). This is because Selenium relies on JavaScript, and the security settings standard in most browsers wouldn't allow it to run unless it is available on the same host and port as your application.
 940  
 941  >**CAUTION**
 942  >Be careful not to transfer the `selenium/` directory to your production server, since it would be accessible by anyone having access to your web document root via the browser.
 943  
 944  Selenium tests are written in HTML and stored in the `web/selenium/tests/` directory. For instance, Listing 15-36 shows a functional test where the home page is loaded, the link click me is clicked, and the text "Hello, World" is looked for in the response. Remember that in order to access the application in the `test` environment, you have to specify the `myapp_test.php` front controller.
 945  
 946  Listing 15-36 - A Sample Selenium Test, in `web/selenium/test/testIndex.html`
 947  
 948      [php]
 949      <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
 950      <html>
 951      <head>
 952        <meta content="text/html; charset=UTF-8" http-equiv="content-type">
 953        <title>Index tests</title>
 954      </head>
 955      <body>
 956      <table cellspacing="0">
 957      <tbody>
 958        <tr><td colspan="3">First step</td></tr>
 959        <tr><td>open</td>              <td>/myapp_test.php/</td> <td>&nbsp;</td></tr>
 960        <tr><td>clickAndWait</td>      <td>link=click me</td>    <td>&nbsp;</td></tr>
 961        <tr><td>assertTextPresent</td> <td>Hello, World!</td>    <td>&nbsp;</td></tr>
 962      </tbody>
 963      </table>
 964      </body>
 965      </html>
 966  
 967  A test case is represented by an HTML document containing a table with three columns: command, target, and value. Not all commands take a value, however. In this case, either leave the column blank or use `&nbsp;` to make the table look better. Refer to the Selenium website for a complete list of commands.
 968  
 969  You also need to add this test to the global test suite by inserting a new line in the table of the `TestSuite.html` file, located in the same directory. Listing 15-37 shows how.
 970  
 971  Listing 15-37 - Adding a Test File to the Test Suite, in `web/selenium/test/TestSuite.html`
 972  
 973      ...
 974      <tr><td><a href='./testIndex.html'>My First Test</a></td></tr>
 975      ...
 976  
 977  To run the test, simply browse to
 978  
 979      http://myapp.example.com/selenium/index.html
 980  
 981  Select Main Test Suite, click the button to run all tests, and watch your browser as it reproduces the steps that you have told it to do.
 982  
 983  >**NOTE**
 984  >As Selenium tests run in a real browser, they also allow you to test browser inconsistencies. Build your test with one browser, and test them on all the others on which your site is supposed to work with a single request.
 985  
 986  The fact that Selenium tests are written in HTML could make the writing of Selenium tests a hassle. But thanks to the Firefox Selenium extension ([http://seleniumrecorder.mozdev.org/](http://seleniumrecorder.mozdev.org/)), all it takes to create a test is to execute the test once in a recorded session. While navigating in a recording session, you can add assert-type tests by right-clicking in the browser window and selecting the appropriate check under Append Selenium Command in the pop-up menu.
 987  
 988  You can save the test to an HTML file to build a test suite for your application. The Firefox extension even allows you to run the Selenium tests that you have recorded with it.
 989  
 990  >**NOTE**
 991  >Don't forget to reinitialize the test data before launching the Selenium test.
 992  
 993  Summary
 994  -------
 995  
 996  Automated tests include unit tests to validate methods or functions and functional tests to validate features. Symfony relies on the lime testing framework for unit tests and provides an `sfTestBrowser` class especially for functional tests. They both provide many assertion methods, from basic to the most advanced, like CSS selectors. Use the symfony command line to launch tests, either one by one (with the `test-unit` and `test-functional` tasks) or in a test harness (with the `test-all` task). When dealing with data, automated tests use fixtures and stubs, and this is easily achieved within symfony unit tests.
 997  
 998  If you make sure to write enough unit tests to cover a large part of your applications (maybe using the TDD methodology), you will feel safer when refactoring internals or adding new features, and you may even gain some time on the documentation task.


Généré le : Fri Mar 16 22:42:14 2007 par Balluche grâce à PHPXref 0.7