This guide documents the major functionality that JPUP provides. It can be considered a user reference guide with cookbook elements.
This guide does not contain API documentation, nor does it cover topics that might be of interest to developers who wish to make changes to JPUP itself.
This guide assumes that you are already familiar with PHPUnit and unit-testing PHP applications.
For an introduction to unit testing and how to write PHPUnit test cases, see the PHPUnit manual.
The organization of PHPUnit tests is very similar to the way Symfony tests are laid out.
All tests should be stored in the test
directory under the project root
directory (matches the value of sfConfig::get('sf_test_dir')
).
Unit tests should be stored under sf_test_dir/unit
. For best results, create
subdirectories and name your test case files to match the corresponding
library files that they are testing.
For example, the unit tests for lib/model/Profile.class.php
should be stored
in sf_test_dir/unit/lib/model/Profile.class.php
.
- JPUP does not care what the files are named, but by following this file naming convention, you can leverage readline's autocomplete feature when you are running tests via PHPUnit Symfony tasks.
Functional tests should be stored under sf_test_dir/functional
. Use
subdirectories to group functional tests by application and module.
For example, the functional tests for accountActions->executeRegister()
(e.g.,
http://www.example.com/account/register) should be stored in
sf_test_dir/functional/frontend/account/register.php
.
Note that this differs from the way Symfony's built-in test framework organizes
functional tests; Symfony's module generator creates e.g.,
sf_test_dir/functional/frontend/accountActionsTest.php
.
- Just as with unit tests, JPUP does not actually care how you organize your tests. You can create a single functional test case for each module if you want; we just found that it's more efficient for us to locate and run tests when they are organized by module and action.
Writing a unit test for JPUP is very similar to writing test cases for vanilla PHPUnit, but there are a few differences to keep in mind:
-
Have your test class extend
Test_Case_Unit
, notPHPUnit_Framework_TestCase
. -
For setup and teardown functionality, define the
_setUp()
and_tearDown()
methods, respectively, in your test class (note the leading underscore in the method names).
Here is an example of what a unit test class looks like:
# sf_test_dir/unit/lib/Widget/WidgetService.class.php <?php class WidgetServiceTest extends Test_Case_Unit { private /** @var WidgetService_Http_Client_Mock */ $_client; protected function _setUp( ) { /* Inject mock HTTP adapter so that we can simulate/control Widget * Service responses. */ $this->_client = new WidgetService_Http_Client_Mock(); WidgetService::setHttpClient($this->_client); } public function testGetNumberOfLikes( ) { $like_count = '25'; $object_type = 'page'; $object_id = '123'; /* Seed the response from the Widget server. */ $this->_client->seed( sprintf( '/likes/count?object_type=%s&object_id=%d', $object_type, $object_id ), json_encode(array( 'status' => 'OK', 'likes' => $like_count )) ); /* Execute the API method and check the result. */ $this->assertEquals( $like_count, WidgetService::getNumberOfLikes($object_type, $object_id), 'Expected correct number of likes returned.' ); } }
JPUP comes packaged with a Symfony task named phpunit:generate-unit
to build
unit tests for you automatically.
To use phpunit:generate-unit
, you must first create the class skeleton, as the
task will use Symfony's auto-loader to locate the class file. You can also
generate unit tests for a class that you have already added methods to (the
task will create skeleton tests for you).
For example, suppose you wanted to create a test case for this class:
# sf_lib_dir/HelloWorld.class.php <?php /** My first PHP class! * * @package myproject * @subpackage helloworld */ class HelloWorld { /** Returns everyone's favorite phrase. * * @return string */ public function getString( ) { return 'Hello, World!'; } /** Returns a generic string representation of the object. * * @return string */ public function __toString( ) { return $this->getString(); } }
To build a skeleton test case for the HelloWorld
class as defined in the above
example, execute the following command:
./symfony phpunit:generate-unit HelloWorld
Note that you must pass the class name to the task. This is a requirement to avoid ambiguity when a class file contains multiple class definitions.
- JPUP does not support generating tests for multiple classes in the same file. If you want test cases for multiple classes that are defined in the same file, you will either need to create the test cases by hand or split out the classes into separate files.
phpunit:generate-unit
will create a test case for you in sf_test_dir/unit
,
and it will create subdirectories as needed so that the directory structure
matches the location of the class file.
In the example above, the test case file will be created at
sf_test_dir/unit/lib/HelloWorld.class.php
that looks something like this:
# sf_test_dir/unit/lib/HelloWorld.class.php <?php /** Unit tests for HelloWorld. * * @author PHX * * @package myproject * @subpackage test.helloworld */ class HelloWorldTest extends Test_Case_Unit { protected function _setUp( ) { } public function testGetString( ) { $this->markTestIncomplete('Not implemented yet.'); } }
Note that JPUP automatically populates the @package
, @subpackage
and
@author
phpdoc tags from the class docblock, and it creates skeleton tests
for any public, non-magic methods it finds in the class.
-
If the class does not have a
@package
tag, JPUP will try to use the name of the project as defined insf_config_dir/properties.ini
. -
If the class does not have a
@subpackage
tag, JPUP will try to guess one based on the class file's location in the project's directory structure. -
If the class does not have an
@author
tag, JPUP will try to use the name of the project author as defined insf_config_dir/properties.ini
. -
The template for the skeleton test case is located in
sf_root_dir/plugins/sfJwtPhpUnitPlugin/lib/task/phpunit/skeleton/unit.php
.If desired, you can create your own template. JPUP will first check for a skeleton file at
sf_data_dir/skeleton/phpunit/unit.php
. -
You may customize the values of any tokens, such as the package or subpackage names (or any additional tokens in your custom skeleton file) by passing
--token
arguments to the task.For example, to change the
@package
of the test case to "MyAwesomeProject", you would invoke the task like this:
./symfony phpunit:generate-unit --token='package:MyAwesomeProject' HelloWorld
Functional tests are very similar to unit tests as described above, but you also
have access to $this->_browser
which is an instance of a modified version of
the sfBrowser
class: Test_Browser
.
Also, functional test classes should extend the Test_Case_Functional
class
rather than Test_Case_Unit
.
Here is an example of a functional test class:
# sf_test_dir/functional/frontend/account/register.php <?php class frontend_account_registerTest extends Test_Case_Functional { public function testSuccess( ) { /* Activate additional browser plugins. */ $this->_browser->usePlugin('form', 'mailer'); $username = 'mytester'; $password = 'password'; $email = 'tester@jwt.com'; /* Send browser to the registration form page. */ $this->_browser->get('/account/register'); $this->assertStatusCode(200); /* Simulate form submission. */ $this->_browser->click('Submit', array( 'username' => $username, 'password' => $password, 'email' => $email )); /* Check assertions. */ $this->assertFalse( $this->_browser->getForm()->hasErrors(), 'Expected form to have no errors.' ); $this->assertNotNull( Doctrine::getTable('Profile')->retrieveByUsername($username), 'Expected Profile record to be created successfully.' ); $Mailer = $this->_browser->getMailer(); $this->assertEquals( 1, $Mailer->countMessages(), 'Expected welcome email to be dispatched.' ); $this->assertEquals( $email, $Mailer->getMessage(0)->getTo(), 'Expected welcome email to be sent to the user.' ); $this->assertEquals( 'account/home', $this->_browser->getResponse()->getRedirectUrl(), 'Expected browser to be redirected to account homepage.' ); /* Follow the redirect. */ $this->_browser->followRedirect(); $this->assertStatusCode(200); $this->assertEquals( sprintf('Welcome, %s!', $username), $this->_browser->getContent()->select('#welcome')->getValue(), "Expected success page to display user's new username." ); } }
JPUP comes packaged with a Symfony task named phpunit:generate-functional
to
build functional tests for you automatically.
To use phpunit:generate-functional
, you must first create the module and
action you wish to test and wire it into your application's routing.yml
.
Once that is done, invoke the task like this:
./symfony phpunit:generate-functional <route>
Where <route>
is either a route name (prefixed with @
) or a module/action
pair.
For example, consider if your application's routing.yml
looked like this:
# sf_app_dir/config/routing.yml # default rules homepage: url: / param: { module: default, action: index } # generic rules # please, remove them by adding more specific rules default_index: url: /:module param: { action: index } default: url: /:module/:action/*
You could generate a functional test case for main/index
using either of the
following commands:
./symfony phpunit:generate-functional @homepage ./symfony phpunit:generate-functional main/index
- By default, JPUP will look at the routing for the "frontend" application. If
you want to generate a functional test for a different application, you will
need to specify it using the
--application
parameter:
./symfony phpunit:generate-functional --application=backend @activitylogs
JPUP will generate a skeleton test case for you that looks something like this:
# sf_test_dir/functional/frontend/main/index.php <?php /** Functional tests for /main/index. * * @author PHX * * @package myproject * @subpackage test.main */ class frontend_main_indexTest extends Test_Case_Functional { protected $_application = 'frontend', $_url; protected function _setUp( ) { $this->_url = '/main/index'; } public function testSmokeCheck( ) { $this->_browser->get($this->_url); $this->assertStatusCode(200); } }
Note that, just like phpunit:generate-unit
, phpunit:generate-functional
automatically populates the @package
, @subpackage
and @author
phpdoc
tags from the class docblock.
-
If the action class does not have a
@package
tag, JPUP will try to use the name of the project as defined insf_config_dir/properties.ini
. -
If the action class does not have a
@subpackage
tag, JPUP will try to guess one based on the module and action names. -
If the class does not have an
@author
tag, JPUP will try to use the name of the project author as defined insf_config_dir/properties.ini
. -
The template for the skeleton test case is located in
sf_root_dir/plugins/sfJwtPhpUnitPlugin/lib/task/phpunit/skeleton/functional.php
.If desired, you can create your own template. JPUP will first check for a skeleton file at
sf_data_dir/skeleton/phpunit/functional.php
. -
You may customize the values of any tokens, such as the package or subpackage names (or any additional tokens in your custom skeleton file) by passing
--token
arguments to the task.For example, to change the
@package
of the test case to "MyAwesomeProject", you would invoke the task like this:
./symfony phpunit:generate-functional --token='package:MyAwesomeProject' main/index
Testing applications that require login is a tricky proposition. It's easy enough to sign a user in, but every time the browser makes a request, it destroys and rebuilds the application context, which logs the user back out!
Test_Browser
provides a signin()
method to solve this problem. Simply pass
in a username or email address, and the browser will make sure the user is
logged in during the next and subsequent requests:
# sf_test_dir/functional/frontend/admin/dashboard.php <?php class frontend_admin_dashboardTest extends Test_Case_Functional { protected $_url = '/admin/dashboard'; public function testMustBeLoggedIn( ) { $this->_browser->get($this->_url); $this->assertStatusCode(401); } public function testUserCanAccessIfSignedIn( ) { $this->loadFixture('admin_user.php'); $this->_browser->signin('administrator'); $this->_browser->get($this->_url); $this->assertStatusCode(200); } public function testSigninOnlyLastsForTheDurationOfTheTest( ) { $this->_browser->get($this->_url); $this->assertStatusCode(401); } }
Note from the last test in the example above that the user will only remain
signed in for the duration of the test in which the call to signin()
was
made. If you want the user to be logged in during another test, you will need
to call signin()
again, or move that code into your test case's _setUp()
method.
While building JPUP, we found that there were a number of features that
the sfTestFunctional
classes afforded that are extraordinarily useful for
testing but are not accessible to sfBrowser
.
After a lot of experimentation, we started developing a series of plugins that encapsulate properties of the Symfony context, adding extra features and exposing more of the objects' internals without restricting their respective APIs.
Here is the full list of plugins that come with JPUP:
-
Content
- Usage:
$this->_browser->getContent()
- HTML response text container.
- Also includes methods for decoding serialized content and interacting with
Symfony's
sfDomCssSelector
.
- Usage:
-
Error
- Usage:
$this->_browser->getError()
- Makes accessible the uncaught exception from the request if applicable (useful for debugging HTTP 500 response codes).
- Usage:
-
Form
- Usage:
$this->_browser->getForm()
- Makes accessible the
sfForm
instance bound to the Symfony action if applicable.
- Usage:
-
Logger
- Usage:
$this->_browser->getLogger()
- Injects an
sfVarLogger
into the context so that log messages can be inspected.
- Usage:
-
Mailer
- Usage:
$this->_browser->getMailer()
- SwiftMailer log (
sfMailerMessageLoggerPlugin
). - Also provides methods for searching messages by header or position.
- Usage:
-
Request
- Usage:
$this->_browser->getRequest()
- Symfony Request object (
sfWebRequest
). - Also provides methods for determining whether and where a request was forwarded.
- Usage:
-
Response
- Usage:
$this->_browser->getResponse()
- Symfony Response object (
sfWebResponse
). - Also provides methods for determining whether and where a response was redirected.
- Usage:
-
ViewCache
- Usage:
$this->_browser->getViewCache()
- Makes accessible the view cache manager (
sfViewCacheManager
).
- Usage:
The Form, Mailer and ViewCache plugins are not used that frequently and need
to be activated before they can be used (by calling
$this->_browser->usePlugin('...')
; see individual plugin sections for
more information).
For complete API documentation of browser plugins, see the PHPDoc-generated documentation.
Here are some examples of use cases where Test_Browser
plugins come in handy:
A number of applications expose services that return serialized or JSON-encoded values instead of HTML content. Manually parsing the content these requests would quickly become tedious; fortunately, the Content plugin provides methods to handle this automatically:
# sf_test_dir/functional/frontend/do/like.php <?php class frontend_do_likeTest extends Test_Case_Functional { public function testSuccess( ) { $this->_browser->post('/do/like', array( 'user_id' => '1', 'object_id' => '123' )); $this->assertStatusCode(200); /* (string) $this->_browser->getContent() would evaluate to: * '{"status":"OK","likes":"1"}' * * To work with the decoded JSON code, use: */ $decoded = $this->_browser->getContent()->decodeJson(); $this->assertEquals( 'OK', $decoded->status, 'Expected success status value.' ); $this->assertEquals( 1, $decoded->likes, 'Expected correctly-incremented number of likes.' ); } }
-
The Content plugin also has a
deserialize()
method for output encoded with PHP'sserialize()
function. -
Note:
$this->_browser->getContent()
returns an instance ofTest_Browser_Plugin_Content
. If you want the raw text from the response, cast it as a string or invoke its__toString()
method.
To access a submitted form, use the Form plugin:
# sf_test_dir/functional/frontend/contactus/reportissue.php <?php class frontend_main_reportissueTest extends Test_Case_Functional { public function testSuccess( ) { /* The Form plugin has to be activated before it can be used. */ $this->_browser->usePlugin('form'); $this->_browser->get('/contactus/reportissue'); $this->assertStatusCode(200); $this->_browser->click('Submit', array( 'issue' => array( 'firstname' => 'Functional', 'lastname' => 'Tester', 'email' => 'functional_tester@jwt.com', 'description' => 'This is a test form submission.' ) )); /* Access the submitted form. */ $Form = $this->_browser->getForm(); $this->assertFalse( $Form->hasErrors(), 'Expected form to have no errors.' ); } }
-
As it is not used very often, the Form plugin is not enabled by default. To use it in your test, call
$this->_browser->usePlugin('form')
. -
The Form plugin will only fetch a single
sfForm
instance from the action stack. If an action utilizes more than one bound form, this plugin will only return one of them (whichever one was assigned to the action's variable holder first). -
If a form is not added to the action's variable holder (e.g., by assigning it to
$this->form
in the action), it will not be accessible to JPUP.
To interact with Symfony's built in mailer, use the Mailer plugin:
# sf_test_dir/functional/frontend/contactus/reportissue.php <?php class frontend_main_reportissueTest extends Test_Case_Functional { public function testSuccess( ) { /* The Mailer plugin has to be activated before it can be used. */ $this->_browser->usePlugin('mailer'); $email = 'functional_tester@jwt.com'; $this->_browser ->get('/contactus/reportissue') ->click('Submit', array( 'issue' => array( 'firstname' => 'Functional', 'lastname' => 'Tester', 'email' => $email, 'description' => 'This is a test form submission.' ) )); ... snip ... /* Test emails sent from form submission. */ $Mailer = $this->_browser->getMailer(); $this->assertEquals( 2, $Mailer->countMessages(), 'Expected correct number of emails to be sent.' ); /* Get the email that was sent to the user. */ $Message = $Mailer->getMessageWith('to', $email); $this->assertNotNull( $Message, 'Expected an email to be sent to the user.' ); $this->assertEquals( sfConfig::get('app_webmaster_email'), $Message->getFrom(), 'Expected email sent to user to be from system administrator.' ); } }
-
As it is not used very often, the Mailer plugin is not enabled by default. To use it in your test, call
$this->_browser->usePlugin('mailer')
. -
The Mailer plugin only interacts with Symfony's built-in SwiftMailer emailer. If you are using your own mailer, you will need to write your own interface for interacting with it for testing.
The Request and Response plugins provide access to forwarding and redirecting information, respectively.
- For an explanation of the difference between redirecting and forwarding, see this blog post.
# sf_test_dir/functional/frontend/contactus/reportissue.php <?php class frontend_contactus_reportissueTest extends Test_Case_Functional { public function testSubmission( ) { $this->_browser->get('/contactus/reportissue'); $this->assertStatusCode(200); $this->_browser->click('Submit', array(...)); ... snip ... $this->assertEquals( '/contactus/reportissue/thankyou', $this->_browser->getResponse()->getRedirectURL(), 'Expected browser to be redirected to the confirmation page.' ); } public function testForwardIfNoSubmission( ) { $this->_browser->get('/contactus/reportissue/thankyou'); $this->assertEquals( 'contactus/reportissue', $this->_browser->getRequest()->getForwardString(), 'Expected request to be forwarded to form.' ); } }
-
The test browser will not follow redirects automatically. To follow a redirect, call
$this->_browser->followRedirect()
.This only applies to redirects; forwards are followed automatically.
When applications generate 500 errors, Symfony will forward the request to a generic error page, which makes troubleshooting these problems in functional tests particularly frustrating.
Fortunately, the Error plugin makes it easy to get information (including a stack trace) about any uncaught exceptions the application generates:
# sf_test_dir/functional/frontend/contactus/reportissue.php <?php class frontend_main_reportissueTest extends Test_Case_Functional { public function testSuccess( ) { $this->_browser ->get('/contactus/reportissue') ->click('Submit', array( 'issue' => array( 'firstname' => 'Functional', 'lastname' => 'Tester', 'email' => $email, 'description' => 'This is a test form submission.' ) )); /* For some reason, the request is generating a 500 error. Find out what * the problem is: */ echo PHP_EOL, PHP_EOL, $this->_browser->getError() PHP_EOL, PHP_EOL, $this->_browser->getError()->getTraceAsString(); /* This assertion will still fail, but not before we get to see what's going * on. */ $this->assertStatusCode(200); } }
- Since 500 errors are generally not considered to be desirable behavior, you will probably end up using this plugin to debug your application rather than as part of a test or assertion.
Occasionally, it can be useful to look at the log messages generated by a
particular request. By default, Symfony will log messages to
sf_log_dir/appname_test.log
, but this file tends to get rather large, and
it can be cumbersome to scroll through it to identify the failed request.
JPUP provides an alternative interface for interacting with Symfony's logger.
The Logger plugin will inject an sfVarLogger
instance into the context so
that you can inspect log messages in your test.
To inject the logger, you will need to first call
$this->_browser->usePlugin('logger')
.
Once the logger has been injected, you can inspect the log messages generated by the most recent request like this:
# sf_test_dir/functional/frontend/contactus/reportissue.php <?php class frontend_main_reportissueTest extends Test_Case_Functional { public function testSuccess( ) { /* Inject the logger so that we can inspect log messages. */ $this->_browser->usePlugin('logger'); $this->_browser ->get('/contactus/reportissue') ->click('Submit', array( 'issue' => array( 'firstname' => 'Functional', 'lastname' => 'Tester', 'email' => $email, 'description' => 'This is a test form submission.' ) )); /* For some reason, the request is generating a 404. Maybe the application * logs will hold some clues. */ echo $this->_browser->getLogger(), PHP_EOL; /* This assertion will still fail, but not before we get to see what's going * on. */ $this->assertStatusCode(200); } }
Note that the logger will be injected after the context loads its factories, so there will not be any log messages from factory initialization (such as connecting routes).
You can work around this by instructing Symfony to load an sfVarLogger
in
your factories.yml
file. The Logger plugin will first check to see if an
sfVarLogger
has been added to the context before injecting its own.
# sf_apps_dir/frontend/config/factories.yml: test: logger: class: sfAggregateLogger param: level: debug loggers: sf_var_logger: class: sfVarLogger param: level: debug
With the above configuration in place in your application's factories.yml
file, you will be able to inspect all application log messages in your test
case, including any generated while initializing the context's factory
objects (but be aware that this comes at a cost; the sfVarLogger
will be
created and populated regardless of whether you use it!).
Note that JPUP is currently only compatible with Doctrine.
Before running any tests, JPUP first verifies to make sure that a distinct "test" DSN has been specified in databases.yml and that the active Doctrine connection is using the correct DSN.
- If any problems are detected with the test database configuration, JPUP outputs an error message and exits immediately to prevent any tests from running (and potentially corrupting production data).
To specify a separate DSN for testing, add the following lines to your databases.yml file:
# config/databases.yml test: doctrine: class: sfDoctrineDatabase param: dsn: "mysql:host=<hostname>;dbname=<test db name>" username: "<username>" password: "<password>"
Replace the values in brackets above with ones that correspond to your database setup.
-
Make sure that you also create the test database and assign user privileges if necessary.
-
If your application does not use a database, you can disable these checks (and all database connectivity) by setting
use_database
tofalse
inapps/*/config/settings.yml
:
# apps/frontend/config/settings.yml all: .settings: ... snip ... use_database: false
Before running each test, JPUP automatically flushes the database:
-
Before the first test runs, JPUP completely destroys and rebuilds the test database:
-
Drop and recreate the test database.
- This step is roughly equivalent to calling
php ./symfony doctrine:drop-db --env=test
followed byphp ./symfony doctrine:build-db --env=test
.
- This step is roughly equivalent to calling
-
Rebuild all tables from model classes.
-
This step is roughly equivalent to calling
php ./symfony doctrine:insert-sql --env=test
. -
Because tables are built from model classes, there is no need to worry about applying database change scripts or having an up-to-date schema.yml file for testing (though it is still a good idea to have these things for many other reasons!).
-
-
-
For efficiency, subsequent tests will truncate all tables instead of rebuilding the entire database.
Once the data has been flushed, JPUP loads global test data fixtures, if any exist (see below).
Note that JPUP flushes the database before each test, not after it.
This is by design; if you want to inspect the state of the database after a
failed test, use exit()
to halt test execution before the failing assertion,
then use your favorite DB client application to examine the test database.
For efficiency, only global test fixtures are loaded by default (more on this in a bit).
To load additional test fixtures, call $this->loadFixture()
in your test case,
e.g.:
# sf_test_dir/unit/Hello.php <?php class HelloTest extends Test_Case_Unit { protected function _setUp( ) { $this->loadFixture('hello.yml'); } }
-
The database gets flushed in between tests, so you will need to make sure your test case loads the appropriate fixtures before every test that uses them.
- Consider leveraging
_setUp()
for fixtures that must be loaded for every test in a test case.
- Consider leveraging
-
All test fixtures must be located in
sf_test_dir/fixtures
. The fixture loader will not load any fixture files outside this directory.Note that this means that you cannot load production data fixtures (located in
sf_root_dir/data/fixtures
) in your test cases. This is intentional; JPUP is designed to test code, not data. -
By default,
loadFixture()
will not load a fixture more than once during a test. There is a way to force it to load a fixture multiple times (but be wary of infinite loops!). See the API documentation for more information.
As mentioned above, there is a category of fixture that does get loaded automatically before each test, known as global fixtures.
Global fixtures are named like _global.*
(e.g., _global.yml
, _global.php
,
etc.). In other words there can be one global fixture for each fixture type.
Global fixtures are loaded using the same mechanism as other fixtures, so they
must be located in sf_test_dir/fixtures
and have the same limitations and
features as any other fixture.
JPUP supports loading two different fixture types: YAML and PHP.
JPUP can load YAML fixture files similarly to the way Symfony's
doctrine:data-load
task operates. To load a YAML fixture file, provide the
name of the file to $this->loadFixture()
:
# sf_test_dir/unit/MyTest.php <?php class MyTest extends Test_Case_Unit { protected function _setUp( ) { $this->loadFixture('users.yml'); } }
Here's what the YAML fixture looks like:
# sf_test_dir/fixtures/users.yml User: admin: username: admin password: password active: 1 editor: username: editor password: password active: 1 inactive: username: haxor password: 1337 active: 0
As with other Symfony YAML files, you can include PHP code and Symfony config values:
# sf_test_dir/fixtures/users.yml User: admin: username: %APP_DEFAULT_ADMIN_USERNAME% password: <?php echo sha1('saltpasswordsalt'); ?> active: 1
If you need to execute a large amount of PHP code, or if you need to set up inter-fixture relationships, you might find it more effective to use a PHP fixture file instead.
Load a PHP fixture file identically to the way you would load a YAML fixture file, except that the filename will have a '.php' extension rather than '.yml':
# sf_test_dir/unit/lib/MyClass.php <?php class MyClassTest extends Test_Case_Unit { protected function _setUp( ) { $this->loadFixture('articles.php'); } }
A PHP fixture file can contain any PHP code.
- With great power comes great responsibility. Try to keep your fixtures short, simple and focused. The last thing you want is to have to write test cases for your data fixtures!
You can load other fixtures from a PHP fixture file by calling
$this->loadFixture()
just like you would from a test case:
# sf_test_dir/fixtures/articles.php <?php $this->loadFixture('sites.yml'); $this->loadFixture('categories.php');
PHP fixture files can share variables between one other. To make a variable
accessible to other fixture files, assign it as a property of $this
in the
fixture file:
# sf_test_dir/fixtures/articles.php <?php $this->Article = new Article(); $this->Article->setTitle('Hello, world!'); $this->Article->save();
Because articles.php
defines $this->Article
, any subsequently-loaded fixture
can access it:
# sf_test_dir/fixtures/categories.php <?php /* Load dependency fixture. */ $this->loadFixture('articles.php'); $Cat = new Category(); $Cat->setTitle('Standard Content'); /* Associate the Article object from sf_test_dir/fixtures/articles.php with * the Category object. */ $Cat->setArticle($this->Article); $Cat->save();
If you write a fixture that relies on other fixtures being loaded, it is
recommended that you explicitly call $this->loadFixture()
in the fixture
itself to make sure that its dependencies get loaded.
-
In other words, don't rely on the test case to manage fixture dependencies.
-
Remember that
loadFixture()
will not load a fixture more than once per test by default, so when in doubt it is always better to include too many calls toloadFixture()
than too few.
You can also access shared fixture variables in test cases. Use
$this->getFixtureVar()
to access them:
# sf_test_dir/fixtures/site.php <?php $this->TestSite = new Site(); $this->TestSite->setName('Tanis Dig'); $this->TestSite->save();
# sf_test_dir/unit/model/SiteTable.php <?php class SiteTableTest extends Test_Case_Unit { public function testFetchByName( ) { $this->loadFixture('site.php'); /* References $this->TestSite from the fixture file. */ $controlID = $this->getFixtureVar('TestSite')->getId(); $this->assertEquals( $controlID, Doctrine::getTable('Site')->fetchByName('Tanis Dig')->getId(), 'Expected fetched Site object to have correct ID.' ); } }
Because the database gets flushed before every test, it might be necessary to load a given test fixture several times over the course of a test case. This makes it a little tricky to define constants in a test fixture.
The PHP fixture loader provides a solution:
# sf_test_dir/fixtures/articles.php <?php /* Defines the constant TEST_ARTICLE_TITLE if not already defined. */ $this->define('TEST_ARTICLE_TITLE', 'Hello, World!'); $Node = new Article(); $Node->setTitle(TEST_ARTICLE_TITLE); $Node->save();
The constant is, naturally, accessible in test cases as well:
# sf_test_dir/unit/ArticleTable.php <?php class ArticleTableTest extends Test_Case_Unit { public function testFetchByTitle( ) { $this->loadFixture('articles.php'); /* References TEST_ARTICLE_TITLE constant defined in the articles.php * fixture. */ $Article = Doctrine::getTable('Article')->fetchByTitle(TEST_ARTICLE_TITLE); $this->assertFalse( $Article->isNew(), 'Expected fetchByTitle() to find the existing article.' ); } }
As with shared fixture variables, constants defined in fixture files are not
namespaced. If you are trying to define()
a constant that was already set
in another test fixture (or is a built-in PHP constant, set by the Symfony
framework, etc.), it will silently fail.
To avoid this problem, it is recommended that you adopt a naming convention (such as prepending "TEST_" to all test constant names).
You can flush the database manually in your test by calling
$this->flushDatabase()
.
For example, consider this functional test that verifies that content can be exported from one environment and re-imported onto a second environment.
After generating the export files, we flush the database and load a new fixture to simulate a separate instance of the application.
# sf_test_dir/functional/backend/migrate/index.php <?php class backend_migrate_indexTest extends Test_Case_Functional { public function testContentMigration( ) { /* Init the source environment. */ $this->loadFixture('content_migration_source.php'); sfConfig::set('app_which_env', 'test-'); /* Generate content migration files. */ $this->_browser->get('/backend.php/migrate/export', array( 'site_id' => '1', 'dest' => 'test2-' )); /* Verify the export files were created successfully. */ ... assertions go here ... /* Pretend we're now on the destination environment. */ $this->flushDatabase(); $this->loadFixture('content_migration_destination.php'); sfConfig::set('app_which_env', 'test2-'); /* Load content migration files into destination. */ $this->_browser->get('/backend.php/migrate/import', array( 'site_id' => '1', 'from' => 'test-' )); /* Verify that objects were imported successfully. */ ... assertions go here ... } }
-
flushDatabase()
takes an optional$rebuild
parameter that will force it to drop and rebuild the entire database rather than just truncating all the data.Note that JPUP does not rebuild the database between tests; it only flushes the data. If you are testing a script that modifies the structure of the database, be sure to call
$this->flushDatabase(true)
when the test is finished (preferably in such a way that a failed assertion won't cause it to get skipped!).
JPUP requires that your project have a separate uploads directory for testing so that test execution doesn't overwrite production files.
Even if the code you are testing does not use file uploads explicitly, JPUP
still requires that you define a separate test upload directory, as the
Test_Browser
class might automatically write to that directory during a
post()
operation.
- Plus, it's one less thing to worry about when you start testing actions that do accept file uploads.
To specify an uploads directory for JPUP, add the following line to the settings.yml file for your application:
# apps/frontend/config/settings.yml test: .settings ... snip ... # Add this line: upload_dir: %SF_TEST_CACHE_DIR%/uploads
- You will also need to make sure that the test uploads directory exists and is writable.
JPUP will automatically remove all files in the test uploads directory before each test.
If you need to clear out the uploads directory mid-test, you can call
$this->flushUploads()
:
# sf_test_dir/functional/frontend/account/profile.php <?php class frontend_account_profileTest extends Test_Case_Functional { public function testDetectMissingAvatar( ) { /* User uploads an avatar as normal. */ $this->_browser ->get('/account/profile/avatar') ->click('Submit', array( 'avatar' => sfConfig::get('sf_fixture_dir') . '/uploads/avatar.jpg' )); /* Pretend the upload inexplicably failed. */ $this->flushUploads(); /* Follow the redirect back to the profile page. */ $this->_browser->followRedirect(); $this->assertEquals( sfConfig::get('app_missing_avatar'), $this->_browser->getContent()->select('#avatar')->getAttribute('src'), 'Expected "missing avatar" graphic to display in place of missing avatar.' ); } }
JPUP automatically restores sfConfig
values between tests, so you do not have
to manually reset any sfConfig
changes during your tests.
If you wish to revert all sfConfig
values mid-test, call
$this->flushConfigs()
:
# sf_test_dir/unit/lib/ConfigWatcher.class.php <?php class ConfigWatcherTest extends Test_Case_Unit { public function testSuccess( ) { ConfigWatcher::init(); /* Change a config value. */ $key = 'app_some_value'; sfConfig::set($key, 100); $this->assertTrue( ConfigWatcher::isModified($key), 'Expected ConfigWatcher to notice when config value was modified.' ); /* Reset the config value. */ $this->flushConfigs(); $this->assertFalse( ConfigWatcher::isModified($key), 'Expected ConfigWatcher to notice when modified config value was reset.' ); } }
By default, Symfony turns off E_NOTICE
errors for the test
environment.
This can prevent PHPUnit from catching genuine logic errors, so JPUP requires
that error_reporting
be set to its most verbose setting in settings.yml.
To fix error_reporting
, look for the following setting in
apps/*/config/settings.yml
:
# apps/frontend/config/settings.yml test: .settings: error_reporting: <?php echo ((E_ALL | E_STRICT) ^ E_NOTICE)."\n" ?>
And change it to look like this:
# apps/frontend/config/settings.yml test: .settings: error_reporting: <?php echo (E_ALL | E_STRICT)."\n"; ?>
- This feature cannot currently be bypassed. If your application relies on
code that generates
E_NOTICE
errors (and you don't want to fix them), you will need to make use of PHP's@
operator.
When running PHPUnit Symfony tasks, JPUP will look for and execute the
bootstrap file in sf_test_dir/bootstrap/phpunit.php
. If you have any code that
needs to be executed before any tests are run, put it in this bootstrap file.
For example, if your project does not have a frontend
application, you will
need to specify a different default application for your tests in the
bootstrap script as noted in the following section.
-
The code in this file is executed before an
sfContext
instance gets initialized. -
This file is optional. If JPUP does not find the bootstrap script, it will not trigger any errors.
By default, JPUP runs tests using the frontend
application context. If your
test (unit or functional) should be run with a different configuration, add an
$_application
property to your test class:
# sf_test_dir/functional/backend/config/set.php <?php class backend_config_SetTest extends Test_Case_Functional { protected $_application = 'backend'; ... snip ... }
-
This feature is primarily intended for functional tests. Unit tests, by definition, should not need to rely on a specific application's configuration. If you find yourself using this feature in your unit tests, you should strongly consider whether you can use dependency injection or some other pattern to decouple your library class from your application.
-
If your project does not have a
frontend
application, you will need to specify a different default value in your bootstrap script:
# sf_test_dir/bootstrap/phpunit.php <?php Test_Case::setDefaultApplicationName('appname');
JPUP includes a number of Symfony tasks that you can use to run your tests:
- To run ALL tests:
php ./symfony phpunit:all
- To run unit tests:
php ./symfony phpunit:unit
- To run functional tests:
php ./symfony phpunit:functional
Note: JPUP is not compatible with Symfony's built-in test tasks. Do not expect
php ./symfony test:*
tasks to work with PHPUnit test cases!
Usage: symfony phpunit:all [-f|--filter="..."] [-g|--groups="..."] [-v|--verbose] Options: --filter (-f) Regex used to filter tests; only tests matching the filter will be run. --groups (-g) Only run tests from the specified group(s). --verbose (-v) If set, PHPUnit will output additional information (e.g. test names). Description: Runs all PHPUnit tests for the project.
- JPUP currently runs all tests in the same PHP instance. Be aware that a fatal
runtime error will be generated if there are tests for two classes in
different applications with the same name (e.g.,
mainActions
).
Usage: symfony phpunit:unit [-f|--filter="..."] [-g|--groups="..."] [-v|--verbose] [path1] ... [pathN] Arguments: path Specify the relative paths to specific test files and/or directories under sf_test_dir/unit. If no arguments are provided, all unit tests will be run. Options: --filter (-f) Regex used to filter tests; only tests matching the filter will be run. --groups (-g) Only run tests from the specified group(s). --verbose (-v) If set, PHPUnit will output additional information (e.g. test names). Description: Runs PHPUnit unit tests for the project.
-
The
path
argument should be a relative path undersf_test_dir/unit
. It can reference directories or files, and you may omit '.php' or '.class.php' (where applicable) from file paths.For example, to run the unit tests located in
sf_test_dir/unit/lib/WidgetService.class.php
andsf_test_dir/unit/lib/model/doctrine/*
, both of these commands will work:./symfony phpunit:unit lib/WidgetService lib/model/doctrine
./symfony phpunit:unit lib/WidgetService.class.php lib/model/doctrine/*
-
If you organize and name your unit test files to mirror your production code files, you can leverage readline's autocompletion feature to save yourself some typing.
Usage: symfony phpunit:functional [-f|--filter="..."] [-g|--groups="..."] [-v|--verbose] [path1] ... [pathN] Arguments: path Specify the relative paths to specific test files and/or directories under sf_test_dir/functional. If no arguments are provided, all functional tests will be run. Options: --filter (-f) Regex used to filter tests; only tests matching the filter will be run. --groups (-g) Only run tests from the specified group(s). --verbose (-v) If set, PHPUnit will output additional information (e.g. test names). Description: Runs PHPUnit functional tests for the project.
-
The
path
argument functions identically to its counterpart forphpunit:unit
, except that the specified paths should be relative tosf_test_dir/functional
.For example, to run the functional tests located in
sf_test_dir/functional/frontend/api/publish.php
andsf_test_dir/functional/frontend/ajax/*
, both of these commands will work:./symfony phpunit:functional frontend/api/publish frontend/ajax
./symfony phpunit:functional frontend/api/publish.php frontend/ajax/*
-
JPUP currently runs all tests in the same PHP instance. Be aware that a fatal error will be generated if there are tests for two classes in different applications with the same name (e.g.,
mainActions
).