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
To execute a symfony task in a test, call $this->runTask()
:
# sf_test_dir/unit/lib/task/HelloTask.class.php
<?php
/** Unit tests for HelloTask.
*
* @author PHX
*
* @package myproject
* @subpackage test.lib
*/
class HelloTaskTest extends Test_Case_Unit
{
protected
$_name = 'hello:user';
public function testExecuteTask( )
{
$user = 'Joey';
$bang = '!';
$status = $this->runTask(
$this->_name,
array($user),
array(
'punctuation' => $bang
);
$this->assertEquals(0, $status, 'Expected task to complete successfully.');
}
}
runTask()
takes up to three parameters:
- The name of the task (required).
- Array of arguments (optional).
- Array of options (optional).
runTask()
returns an integer status code, which is the status code that would
be reported to the shell were this task run via the symfony
command-line
interface.
- Note that at this time there is no way to capture output from the task execution.
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 = '[email protected]';
/* 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
Test_Browser->call()
accepts the same format for URIs as the url_for()
helper, so you can pass in a URI, a route name or even an array of routing
parameters.
For example, suppose you wanted to write a functional test against an action
that has the following entry in routing.yml
:
# sf_app_config_dir/routing.yml
account_contact:
url: /account/contact/:username
class: sfDoctrineRoute
options:
model: sfGuardUser
type: object
param:
module: account
action: show
requirements:
sf_method: [get,post]
The functional test might look something like this:
# sf_test_dir/functional/frontend/account/contact.php
<?php
class frontend_account_contactTest extends Test_Case_Functional
{
public function testViewUserProfile( )
{
$user = new sfGuardUser();
...
$user->save();
/* You can pass the URI directly just like for sfTestFunctional: */
$this->_browser->get('/account/contact/' . $user->getUsername());
$this->assertStatusCode(200);
/* Or use the route name and parameters: */
$this->_browser->get('@account_contact?username=' . $user->getUsername());
$this->assertStatusCode();
/* Or pass an array of route parameters: */
$this->_browser->get(array(
'sf_route' => 'account_contact',
'sf_subject' => $user
));
$this->assertStatusCode(200);
}
}
Because Test_Browser
is based on sfBrowser
, its call()
method also accepts
a $parameters
argument, but note that these parameters are added to the
request after parsing the route.
It is recommended that you only use $parameters
to specify POST
data:
# sf_test_dir/functional/frontend/account/contact.php
<?php
class frontend_account_contactTest extends Test_Case_Functional
{
public function testSendUserAnEmail( )
{
$user = new sfGuardUser();
...
$user->save();
try
{
/* This will cause an error because the 'username' parameter will not
* get included when parsing the route:
*/
$this->_browser->post('@account_contact' array(
'username' => $user->username,
'message' => 'Hello, username!'
));
$this->fail(
'Expected exception when required route parameter is missing.'
);
}
catch( InvalidArgumentException $e )
{
}
/* This will work as expected: */
$this->_browser->post(
array(
'sf_route' => 'account_show',
'sf_subject' => $user
),
array(
'message' => 'Hello, username!'
)
);
$this->assertStatusCode(200);
}
}
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
$_route = '@admin_dashboard';
public function testMustBeLoggedIn( )
{
$this->_browser->get($this->_route);
$this->assertStatusCode(401);
}
public function testUserCanAccessIfSignedIn( )
{
$this->loadFixture('admin_user.php');
$this->_browser->signin('administrator');
$this->_browser->get($this->_route);
$this->assertStatusCode(200);
}
public function testSigninOnlyLastsForTheDurationOfTheTest( )
{
$this->_browser->get($this->_route);
$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($var = null)
- 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:
-
Var
- Usage:
$this->_browser->getVar($var)
- Returns any variable that has been added to the action's var holder (made accessible to the view).
- Usage:
-
ViewCache
- Usage:
$this->_browser->getViewCache()
- Makes accessible the view cache manager (
sfViewCacheManager
).
- Usage:
The Form, Mailer, Var 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('@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('@reportissue');
$this->assertStatusCode(200);
$this->_browser->click('Submit', array(
'issue' => array(
'firstname' => 'Functional',
'lastname' => 'Tester',
'email' => '[email protected]',
'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')
. -
By default, the Form plugin will only fetch a single bound
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 you need to retrieve a specific form instance, you can pass its name to
getForm()
. As an example, consider the following action:# sf_app_module_dir/feedback/actions/actions.class.php class feedbackActions extends sfActions { public function executeContact( sfWebRequest $request ) { $this->cForm = new ContactForm(); $this->riForm = new ReportIssueForm(); if( $request->getMethod() == sfWebRequest::POST ) { $this->cForm->bind( $request->getPostParameter($this->cForm->getName()) ); ... $this->riForm->bind( $request->getPostParameter($this->riForm->getName()) ); } ... } }
To interact with the
ReportIssueForm
instance in your test case, you would need to access it like this:# sf_test_dir/functional/frontend/feedback/contact.php class frontend_feedback_contactTest extends Test_Case_Functional { protected $_application = 'frontend', $_url = '@contactus'; public function testReportIssue( ) { $this->_browser->usePlugin('form'); $this->_browser->post($this->_url, array(...)); $this->assertStatusCode(200); /* Extract the ReportIssueForm from the context. */ $form = $this->_browser->getForm('riForm'); ... } }
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 = '[email protected]';
$this->_browser
->get('@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.
# sf_test_dir/functional/frontend/contactus/reportissue.php
<?php
class frontend_contactus_reportissueTest extends Test_Case_Functional
{
public function testSubmission( )
{
$this->_browser->get('@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('@reportissue_confirmation');
$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.
-
For an explanation of the difference between redirecting and forwarding, see this blog post.
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('@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.
Often in conjunction with troubleshooting 500 errors, it can be useful to inspect variables that are sent to the view.
To extract a variable from the test browser context, use the Var plugin:
# sf_app_module_dir/main/actions/actions.class.php
class mainActions extends sfActions
{
public function executeCategory( sfWebRequest $request )
{
$cat = $request->getParameter('category');
...
$this->listings = ListingTable::getInstance()->findByCategory($cat);
}
}
# sf_test_dir/functional/frontend/main/category.php
class frontend_main_categoryTest extends Test_Case_Functional
{
protected
$_application = 'frontend',
$_route = '@category_view';
public function testValidCategory( )
{
$this->_browser->usePlugin('var');
$this->_browser->get($this->_route, array('category' => 'foo'));
$this->assertStatusCode(200);
/* Extract listings that were sent to the view. */
$listings = $this->_browser->getVar('listings');
}
}
-
The Var plugin interacts with the action's var holder. If a variable is not added to the var holder, the Var plugin will not be able to find it (for example, the
$cat
variable in the above example is not accessible to the test). -
As it is not used very often, the Var plugin is not enabled by default. To use it in your test, call
$this->_browser->usePlugin('var')
.
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('@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_app_config_dir/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!).
From time to time, it might be useful for tests to inject event handlers into the browser context. Since the browser creates a new context for every request, this can be tricky to pull off.
Test_Browser_Listener_Callback
was created to make it easier to accomplish
exactly this. To inject an event handler into the browser context, create a
new instance of Test_Browser_Listener_Callback
and pass it to
Test_Browser->addListener()
.
As an example, consider this test which checks to see if an event handler can
abort the signin process by setting the event's return value to false
:
# sf_test_dir/functional/frontend/auth/signin.php:
class frontend_auth_signinTest extends Test_Case_Functional
{
public function testEventHandlerBlocksSignin( )
{
/* Inject event handler that listens for pre-signin event. */
$this->_browser->addListener(new Test_Browser_Listener_Callback(
'auth.user.signin.pre',
array($this, '_blockSignin')
));
/* Simulate submission of login form. */
$this->_browser->post('@signin', array(
'username' => 'foo',
'password' => 'bar'
));
$this->assertFalse(
$this->_browser->getUser()->isAuthenticated(),
'Expected user to remain unauthenticated.'
);
}
/** Aborts the signin process by setting the event's return value to
* false.
*
* @param sfEvent $event
*
* @return void
*/
public function _blockSignin( sfEvent $event )
{
$event->setReturnValue(false);
}
}
Test_Browser_Listener_Callback->__construct()
accepts a single event name and
one or more callbacks; e.g.:
$this->_browser->addListener(new Test_Browser_Listener_Callback(
'context.load_factories',
/* Call $this->_initContext() when factories are loaded: */
array($this, '_initContext'),
/* Call static method MyClass::doSomething(): */
array('MyClass', 'doSomething'),
/* PHP 5.3 closures are also supported: */
function( sfEvent $event ) { ... }
// etc.
));
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.
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.
To load 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( )
{
/* Load sf_test_dir/fixtures/hello.yml. */
$this->loadFixture('hello.yml');
}
}
To load a fixture for another plugin, pass the plugin's name as the third
parameter to loadFixture()
:
# sf_test_dir/unit/Hello.php
<?php
class HelloTest extends Test_Case_Unit
{
protected function _setUp( )
{
/* Load sf_plugins_dir/myOtherPlugin/test/fixtures/hello.yml. */
$this->loadFixture('hello.yml', false, 'myOtherPlugin');
}
}
Sometimes, it is necessary to load production data fixtures (located in
sf_data_dir/fixtures
) during tests. To load these data fixtures, call
$this->loadProductionFixture()
instead, e.g.:
# sf_test_dir/unit/Ticket.php
<?php
class TicketTest extends Test_Case_Unit
{
protected function _setUp( )
{
/* Load sf_data_dir/fixtures/ticket_types.yml. */
$this->loadProductionFixture('ticket_types.yml');
}
}
loadProductionFixture()
can also be directed to load production fixtures for
other plugins just like loadFixture()
:
# sf_test_dir/unit/Ticket.php
<?php
class TicketTest extends Test_Case_Unit
{
protected function _setUp( )
{
/* Load sf_plugins_dir/myOtherPlugin/data/fixtures/ticket_types.yml. */
$this->loadProductionFixture('ticket_types.yml', false, 'myOtherPlugin');
}
}
-
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
-
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.
JPUP supports loading three different fixture types: SQL, YAML, PHP.
A SQL data fixture is simply a SQL script. When loading a SQL data fixture,
JPUP loads the contents of the file and passes it to the ORM's exec()
method.
Here is an example of a SQL data fixture in action:
# sf_test_dir/unit/MyTest.php
<?php
class MyTest extends Test_Case_Unit
{
public function testFindCategoryByName( )
{
$this->loadFixture('category.sql');
$category = My::category();
$this->assertInstanceOf('Category', $category,
'Expected Category record to be loaded.'
);
$this->assertEquals(My::DEFAULT_CATEGORY, $category->name,
'Expected loaded Category to have correct name.'
);
}
}
# sf_test_dir/fixtures/category.sql
INSERT INTO `category`
SET `name` = 'DEFAULT';
- As the simplest data fixture that JPUP supports, SQL fixtures load the most quickly and are easy to understand, even by developers who are unfamiliar with JPUP or even the ORM.
- Fixture data can be loaded easily into a test database outside the context of running a test, making it easier to load fixtures for spikes and other exploratory operations.
- SQL data fixtures do not leverage any ORM functionality such as behaviors, listeners, validators, events, etc. It is quite possible to create situations using SQL data fixtures that would never occur under normal program operation (some might argue that this is a feature).
- SQL data fixtures also do not leverage PHP constants and other "magic values". Care should be taken when changing these values to ensure that the values are changed both in the application code and any applicable test fixtures.
- For database-agnostic applications, care should be taken to ensure that the syntax in SQL data fixtures are also database-agnostic to prevent false failures when tests are run with incompatible databases.
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, although Symfony configuration values are not currently expanded:
# sf_test_dir/fixtures/users.yml
User:
admin:
# This will not work as expected; username will be literally
# "%APP_DEFAULT_ADMIN_USERNAME%":
username: %APP_DEFAULT_ADMIN_USERNAME%
# This will work as expected; password will be
# "0698f86248c9592589005ba8b7f1e2e9383964cf":
password: <?php echo sha1('saltpasswordsalt'); ?>
- Symfony also uses the YAML format for its production data fixtures, so the syntax will be familiar even to developers that are unfamiliar with JPUP.
- The YAML format is slower to parse than raw SQL fixtures, and it much is less powerful than PHP fixtures.
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()
and/or $this->loadProductionFixture()
just like you
would from a test case:
# sf_test_dir/fixtures/articles.php
<?php
$this->loadProductionFixture('sites.yml');
$this->loadFixture('categories.php');
// Load fixture from another plugin:
$this->loadFixture('entity_types.php', false, 'myOtherPlugin');
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).
- PHP fixtures are by far the most sophisticated and powerful of the data fixture formats supported by JPUP, offering a number of features that simply aren't possible to accomplish in the other formats.
- Errors in PHP fixtures are accompanied by full stack traces including file and line numbers, making it much easier to identify problems with PHP fixtures than the other formats.
- Due to their complexity and feature set, PHP fixtures are the most prone to logic errors, false positives/failures, etc.
- Although this is not necessarily the case, PHP fixtures tend to be the slowest to load of all the fixture formats supported by JPUP, due to the amount of overhead incurred (both from the ORM and JPUP).
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
{
protected
$_application = 'backend';
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('@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('@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.
By default, 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, it is
recommended that you set the $_alwaysRebuildDB
property of your test case
to true
so that the database will be destroyed and rebuilt before every
test in the test case:
# sf_test_dir/unit/lib/db/DBDestroyer.class.php
<?php
class DBDestroyerTest extends Test_Case_Unit
{
protected
$_alwaysRebuildDB = true;
...
}
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 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 accept file uploads.
By default, JPUP will define and create a test uploads directory for you at
runtime at %sf_app_cache_dir%/uploads
.
If you wish to override this value, you can specify a different path in your
app's settings.yml
file.
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('@profile_avatar')
->click('Submit', array(
'avatar' => sfConfig::get('sf_fixture_dir') . '/uploads/me.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" pic 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 is modified.'
);
/* Reset the config value. */
$this->flushConfigs();
$this->assertFalse(
ConfigWatcher::isModified($key),
'Expected ConfigWatcher to notice when config value is 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');
It is now possible to contain all unit tests and fixtures for a plugin with that
plugin's directory on the filesystem (functional tests must still be located
in the project sf_test_dir/test/functional
directory).
Store plugin-specific unit tests in sf_plugins_dir/plugin_name/test/unit
,
where plugin_name
is the name of the plugin the test cases belong to.
In your test case, set the $_plugin
instance property to the name of the
plugin:
<?php
# sf_plugins_dir/myHelloPlugin/test/unit/lib/Hello.class.php
class HelloTest extends Test_Case_Unit
{
protected
$_plugin = 'myHelloPlugin';
}
By default, loadFixture()
and loadProductionFixture()
will look in the
plugin's fixture directory, not the project's. If you need to load a project
fixture from a plugin test case, you will need to specify null
as the third
parameter to loadFixture()
and/or loadProductionFixture()
:
# sf_plugins_dir/myHelloPlugin/test/unit/Hello.php
<?php
class HelloTest extends Test_Case_Unit
{
protected
$_plugin = 'myHelloPlugin';
protected function _setUp( )
{
// Load sf_plugins_dir/myHelloPlugin/test/fixtures/hello.yml.
$this->loadFixture('hello.yml');
// Load sf_test_dir/fixtures/hello.yml.
$this->loadFixture('hello.yml', false, null);
// Load sf_plugins_dir/myHelloPlugin/data/fixtures/hello.yml.
$this->loadProductionFixture('hello.yml');
// Load sf_data_dir/fixtures/hello.yml.
$this->loadProductionFixture('hello.yml', false, null);
}
}
Store plugin-specific test fixtures in
sf_plugins_dir/plugin_name/test/fixtures
, where plugin_name
is the name of
the plugin the test fixtures belong to.
By default, loadFixture()
and loadProductionFixture()
will look in the
plugin's fixture directory, not the project's. If you need to load a project
fixture from a plugin test case, you will need to specify null
as the third
parameter to loadFixture()
and/or loadProductionFixture()
:
# sf_plugins_dir/myAwesomePlugin/test/fixtures/categories.php
<?php
// Load sf_plugins_dir/myAwesomePlugin/test/fixtures/category_types.php.
$this->loadFixture('category_types.php');
// Load sf_test_dir/fixtures/category_types.php.
$this->loadFixture('category_types.php', false, null);
// Load sf_plugins_dir/myAwesomePlugin/data/fixtures/category_types.yml.
$this->loadProductionFixture('category_types.yml');
// Load sf_data_dir/fixtures/category_types.yml.
$this->loadProductionFixture('category_types.yml', false, null);
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="..."] [-p|--plugin="..."] [-v|--verbose] [--stop-on-fail] 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). --plugin (-p) Run tests for the specified plugin. --verbose (-v) If set, PHPUnit will output additional information (e.g. test names). --stop-on-fail If set, stop on the first failure or error. 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="..."] [-p|--plugin="..."] [-v|--verbose] [--stop-on-fail] [path1] ... [pathN] Arguments: path Specify the relative paths to specific test files and/or directories under sf_root_dir/test/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). --plugin (-p) Run tests for the specified plugin. --verbose (-v) If set, PHPUnit will output additional information (e.g. test names). --stop-on-fail If set, stop on the first failure or error. 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="..."] [-p|--plugin="..."] [-v|--verbose] [--stop-on-fail] [path1] ... [pathN] Aliases: func Arguments: path Specify the relative paths to specific test files and/or directories under sf_root_dir/test/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). --plugin (-p) Run tests for the specified plugin. --verbose (-v) If set, PHPUnit will output additional information (e.g. test names). --stop-on-fail If set, stop on the first failure or error. 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. To avoid classloader problems, it is recommended that you run functional tests for one application at a time.