Functional testing without Selenium ¶
Would you like to create functional tests in symfony2 style without Selenium?
What could be easier! Just install `wunit
` extension.
Note that `wunit
based on some
symfony2
classes and support all
symfony2
testing features
(of course, except for directly related to
symfony2
` core like profiling)
Resources ¶
Requirements ¶
- PHP 5.3.x or higher
- PHPUnit 3.6.x or higher
- XDebug extension installed
Installation ¶
1) Download and unpack source into protected/extensions/wunit folder.
2) Import wunit into test config (protected/config/test.php):
# protected/config/test.php
return array(
...
'import' => array(
...
'ext.wunit.*',
),
...
'components' => array(
...
'wunit' => array(
'class' => 'WUnit'
),
...
);
3) Update protected/tests/bootstrap.php
Replace line ~~~ Yii::createWebApplication($config); ~~~ with
require(dirname(__FILE__) . '/../extensions/wunit/WUnit.php');
WUnit::createWebApplication($config);
Finally you should get something like:
$yiit=dirname(__FILE__).'/../../../framework/yiit.php';
$config=dirname(__FILE__).'/../config/test.php';
require_once($yiit);
require(dirname(__FILE__) . '/../extensions/wunit/WUnit.php');
WUnit::createWebApplication($config);
4) Replace protected/tests/phpunit.xml with:
[xml]
<phpunit
bootstrap="bootstrap.php"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
printerClass="WUnit_ResultPrinter"
printerFile="../extensions/wunit/PHPUnit/ResultPrinter.php"
stopOnFailure="false"
/>
NOTICE that `printerClass
and
printerFile
` options are very important.
5*) To test file uploading you should use UploadedFile class instead of CUploadedFile. Here is example:
# protected/config/main.php
return array(
...
'import' => array(
...
'ext.wunit.*',
),
);
# protected/controllers/TestController.php
public function actionFormWithFile() {
$form = new SomeForm();
if (Yii::app()->request->getParam('FullForm')) {
$form->attributes = Yii::app()->request->getParam('FullForm');
$form->fileField = UploadedFile::getInstanceByName("FullForm[fileField]");
if ($form->validate()) {
$form->fileField->saveAs(dirname(__FILE__).'/../files/tmp.txt');
}
}
}
That's it. Now you can use create proper functional tests without Selenium :)
Your First Functional Test ¶
Functional tests are simple PHP files that typically live in the `protected/tests/functional/
folder.
If you would like to test the pages handled by your
SiteController
class, start by creating
a new
SiteControllerTest.php
file that extends a special
WUnitTestCase
` class.
class SiteControllerTest extends WUnitTestCase
{
public function testIndex()
{
$client = static::createClient();
$crawler = $client->request('GET', '/site/index');
$this->assertTrue($crawler->filter('html:contains("Congratulations!")')->count() > 0);
}
}
The `createClient()
` method returns a client, which is like a browser that you'll use to crawl your site:
$crawler = $client->request('GET', '/site/index');
The `request()
method returns a
Crawler
` object which can be used to select elements in the Response, click on links, and submit forms.
The Crawler only works when the response is an XML or an HTML document. To get the raw content response, call $client->getResponse()->getContent().
Click on a link by first selecting it with the Crawler using either an XPath
expression or a CSS selector, then use the Client to click on it. For example,
the following code finds all links with the text `Great
`, then selects
the second one, and ultimately clicks on it:
$link = $crawler->filter('a:contains("Great")')->eq(0)->link();
$crawler = $client->click($link);
Submitting a form is quite similar; select a form button, optionally override some form values, and submit the corresponding form:
$form = $crawler->selectButton('submit')->form();
# set some values
$form['name'] = 'Lucas';
$form['form_name[subject]'] = 'Hey there!';
# submit the form
$crawler = $client->submit($form);
The form can also handle uploads and contains methods to fill in different types
of form fields (e.g. select() and tick()). For details, see the
Forms
section below.
Now when you can easily navigate through an application, use assertions to test
that it actually does what you expect it to do. Use the `Crawler
` to make assertions
on the DOM:
# Assert that the response matches a given CSS selector.
$this->assertTrue($crawler->filter('h1')->count() > 0);
Or, test against the Response content directly if you just want to assert that the content contains some text, or if the Response is not an XML/HTML document:
$this->assertRegExp('/Hello Chris/', $client->getResponse()->getContent());
Run Tests ¶
- Functional testing without Selenium
- Resources
- Requirements
- Installation
- Your First Functional Test
- Working with the Test Client
- The Crawler
- HTTP headers
- Changelog
From protected/tests:
phpunit unit //run all tests from unit folder
phpunit functional //run all tests from functional folder
phpunit functional/SiteControllerTest.php // run specific test
NOTICE WUnit do not require selenium, and if it is not installed on your environment then just comment out the following line in file protected/tests/bootstrap.php
#protected/tests/bootstrap.php
require_once(dirname(__FILE__).'/WebTestCase.php');
More about request() method ¶
The full signature of the `request()
` method is:
request(
$method,
$uri,
array $parameters = array(),
array $files = array(),
array $server = array(),
$content = null,
$changeHistory = true
)
The `server
array is the raw values that you'd expect to normally
find in the PHP
$SERVER` superglobal. For example, to set the Content-Type
and Referer
HTTP headers, you'd pass the following:
$client->request(
'GET',
'/site/page/about',
array(),
array(),
array(
'CONTENT_TYPE' => 'application/json',
'HTTP_REFERER' => '/foo/bar',
)
);
Useful Assertions ¶
To get you started faster, here is a list of the most common and useful test assertions:
# Assert that there is exactly one h2 tag with the class "subtitle"
$this->assertTrue($crawler->filter('h2.subtitle')->count() > 0);
# Assert that there are 4 h2 tags on the page
$this->assertEquals(4, $crawler->filter('h2')->count());
# Assert the the "Content-Type" header is "application/json"
$this->assertTrue($client->getResponse()->headers->contains('Content-Type', 'application/json'));
# Assert that the response content matches a regexp.
$this->assertRegExp('/foo/', $client->getResponse()->getContent());
# Assert that the response status code is 2xx
$this->assertTrue($client->getResponse()->isSuccessful());
# Assert that the response status code is 404
$this->assertTrue($client->getResponse()->isNotFound());
# Assert a specific 200 status code
$this->assertEquals(200, $client->getResponse()->getStatusCode());
# Assert that the response is a redirect to /site/contact
$this->assertTrue($client->getResponse()->isRedirect(Yii::app()->createAbsoluteUrl('/site/contact')));
# or simply check that the response is a redirect to any URL
$this->assertTrue($client->getResponse()->isRedirect());
Working with the Test Client ¶
The test Client simulates an HTTP client like a browser and makes requests to your Yii application:
$crawler = $client->request('GET', '/site/index');
# or
$crawler = $client->request('GET', '/');
The `request()
method takes the HTTP method and a URL as arguments and
returns a
Crawler
` instance.
Use the Crawler to find DOM elements in the Response. These elements can then be used to click on links and submit forms:
$link = $crawler->selectLink('Go elsewhere...')->link();
$crawler = $client->click($link);
$form = $crawler->selectButton('validate')->form();
$crawler = $client->submit($form, array('name' => 'Chris'));
The `click()
and
submit()
methods both return a
Crawler
` object.
These methods are the best way to browse your application as it takes care
of a lot of things for you, like detecting the HTTP method from a form and
giving you a nice API for uploading files.
The `request
` method can also be used to simulate form submissions directly
or perform more complex requests:
# Directly submit a form (but using the Crawler is easier!)
$client->request('POST', '/submit', array('name' => 'Chris'));
# Form submission with a file upload
use Symfony\HttpFoundation\File\UploadedFile;
$photo = new UploadedFile(
'/path/to/photo.jpg',
'photo.jpg',
'image/jpeg',
123
);
# or
$photo = array(
'tmp_name' => '/path/to/photo.jpg',
'name' => 'photo.jpg',
'type' => 'image/jpeg',
'size' => 123,
'error' => UPLOAD_ERR_OK
);
$client->request(
'POST',
'/submit',
array('name' => 'Chris'),
array('photo' => $photo)
);
# Perform a DELETE requests, and pass HTTP headers
$client->request(
'DELETE',
'/post/12',
array(),
array(),
array('PHP_AUTH_USER' => 'username', 'PHP_AUTH_PW' => 'pa$$word')
);
Last but not least, you can force each request to be executed in its own PHP process to avoid any side-effects when working with several clients in the same script:
$client->insulate();
Browsing ¶
The Client supports many operations that can be done in a real browser:
$client->back();
$client->forward();
$client->reload();
# Clears all cookies and the history
$client->restart();
Accessing Internal Objects ¶
If you use the client to test your application, you might want to access the client's internal objects:
$history = $client->getHistory();
$cookieJar = $client->getCookieJar();
You can also get the objects related to the latest request:
$request = $client->getRequest();
$response = $client->getResponse();
$crawler = $client->getCrawler();
Redirecting ¶
When a request returns a redirect response, the client automatically follows
it. If you want to examine the Response before redirecting, you can force
the client to skip following redirects with the `followRedirects()
` method:
$client->followRedirects(false);
When the client does not follow redirects, you can force the redirection with
the `followRedirect()
` method:
$crawler = $client->followRedirect();
The Crawler ¶
A Crawler instance is returned each time you make a request with the Client. It allows you to traverse HTML documents, select nodes, find links and forms.
Traversing ¶
Like jQuery, the Crawler has methods to traverse the DOM of an HTML/XML
document. For example, the following finds all `input[type=submit]
` elements,
selects the last one on the page, and then selects its immediate parent element:
$newCrawler = $crawler->filter('input[type=submit]')
->last()
->parents()
->first()
;
Many other methods are also available:
Method | Description |
---|---|
filter('h1.title') | Nodes that match the CSS selector |
filterXpath('h1') | Nodes that match the XPath expression |
eq(1) | Node for the specified index |
first() | First node |
last() | Last node |
siblings() | Siblings |
nextAll() | All following siblings |
previousAll() | All preceding siblings |
parents() | Parent nodes |
children() | Children |
reduce($lambda) | Nodes for which the callable does not return false |
Since each of these methods returns a new `Crawler
` instance, you can
narrow down your node selection by chaining the method calls:
$crawler
->filter('h1')
->reduce(function ($node, $i)
{
if (!$node->getAttribute('class')) {
return false;
}
})
->first();
Use the `count()
function to get the number of nodes stored in a Crawler:
count($crawler)
`
Extracting Information ¶
The Crawler can extract information from the nodes:
# Returns the attribute value for the first node
$crawler->attr('class');
# Returns the node value for the first node
$crawler->text();
# Extracts an array of attributes for all nodes (_text returns the node value)
# returns an array for each element in crawler, each with the value and href
$info = $crawler->extract(array('_text', 'href'));
# Executes a lambda for each node and return an array of results
$data = $crawler->each(function ($node, $i)
{
return $node->getAttribute('href');
});
Links ¶
To select links, you can use the traversing methods above or the convenient
`selectLink()
` shortcut:
$crawler->selectLink('Click here');
This selects all links that contain the given text, or clickable images for
which the `alt
attribute contains the given text. Like the other filtering
methods, this returns another
Crawler
` object.
Once you've selected a link, you have access to a special `Link
object,
which has helpful methods specific to links (such as
getMethod()
and
getUri()
). To click on the link, use the Client's
click()
method
and pass it a
Link
` object:
$link = $crawler->selectLink('Click here')->link();
$client->click($link);
Forms ¶
Just like links, you select forms with the `selectButton()
` method:
$buttonCrawlerNode = $crawler->selectButton('submit');
Notice that we select form buttons and not forms as a form can have several buttons; if you use the traversing API, keep in mind that you must look for a button.
The `selectButton()
method can select
button
tags and submit
input
`
tags. It uses several different parts of the buttons to find them:
The
`value
` attribute value;The
`id
or
alt
` attribute value for images;The
`id
or
name
attribute value for
button
` tags.
Once you have a Crawler representing a button, call the `form()
method
to get a
Form
` instance for the form wrapping the button node:
$form = $buttonCrawlerNode->form();
When calling the `form()
` method, you can also pass an array of field values
that overrides the default ones:
$form = $buttonCrawlerNode->form(array(
'name' => 'Chris',
'my_form[subject]' => 'Weavora rocks!',
));
And if you want to simulate a specific HTTP method for the form, pass it as a second argument:
$form = $crawler->form(array(), 'DELETE');
The Client can submit `Form
` instances:
$client->submit($form);
The field values can also be passed as a second argument of the `submit()
`
method:
$client->submit($form, array(
'name' => 'Chris',
'my_form[subject]' => 'Weavora rocks!',
));
For more complex situations, use the `Form
` instance as an array to set the
value of each field individually:
# Change the value of a field
$form['name'] = 'Chris';
$form['my_form[subject]'] = 'Weavora rocks!';
There is also a nice API to manipulate the values of the fields according to their type:
# Select an option or a radio
$form['country']->select('France');
# Tick a checkbox
$form['like_weavora']->tick();
# Upload a file
$form['photo']->upload('/path/to/lucas.jpg');
You can get the values that will be submitted by calling the `getValues()
method on the
Form
object. The uploaded files are available in a
separate array returned by
getFiles()
. The
getPhpValues()
and
getPhpFiles()
methods also return the submitted values, but in the
PHP format (it converts the keys with square brackets notation - e.g.
my_form[subject]
` - to PHP arrays).
HTTP headers ¶
If your application behaves according to some HTTP headers, pass them as the
second argument of `createClient()
`:
$client = static::createClient(array(), array(
'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest',
'HTTP_USER_AGENT' => 'MySuperBrowser/1.0',
));
You can also override HTTP headers on a per request basis:
$client->request('GET', '/', array(), array(), array(
'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest',
'HTTP_USER_AGENT' => 'MySuperBrowser/1.0',
));
Changelog ¶
0.2.1
- Fixed issues in unix-like systems
- Enabled followRedirects by default (as per documentation)
0.2
- Added classes autoloader so XPathExpr error should not appear any more
- Fixed exception with YiiExitApplication
- Modified manual to specify printerFile into phpunit.xml
0.1
- Public release
Thumbs up
Great extension
Looks cool!
will try
sounds interesting
to look after :)
Great extension!
I had to change
~~~
class WUnitTestCase extends CTestCase {
~~~
to
~~~
abstract class WUnitTestCase extends CTestCase {
~~~
because otherwise I was getting:
1) Warning No tests found in class "WUnitTestCase".
when running
~~~
[user@host test] phpunit functional
~~~
while running
[user@host test] phpunit functional/SiteTest.php
all was fine.
statusCode not working as I would expect
I am trying to use
$this->assertEquals(200, $client->getResponse()->getStatusCode());
but I am always getting result 200 in the response.
My controller is returning Header(403,"Unauthorized");
and even then var_dump($client->getResponse()); is showing statusCode = 200.
Anyone has any idea?
RE: status code
Hi,
The issue here that there is no way to get response http code into php. More over, there are only one way to get pending to send http headers through xdebug. So we just emulate 3 most used codes.
Please, have a look to wunit/Http/YiiKernel.php
protected function getStatusCode($headers, $error = false) { if ($error) return 503; if (array_key_exists('location', $headers)) return 302; return 200; }
You can change this method for your needs, if you have a way to identify 403 status code in headers.
RE: status code
cool, thanks for a quick answer.
it seems that it will work in PHP 5.4 - http://bugs.xdebug.org/view.php?id=587
For now, I did a workaround.
I am sending an additional header in my controller for errors:
if ($code != 200 ){ header ( 'error-type: ' . $code .' Internal Server Error', true, $code); }
And then in getStatusCode I am checking that custom header:
protected function getStatusCode($headers, $error = false) { if (array_key_exists('error-type',$headers)){ if (preg_match('/^([0-9]{3}) /',$headers['error-type'][0], $matches)){ return $matches[1]; } } if ($error) return 503; if (array_key_exists('location', $headers)) return 302; return 200; }
and that works... now I can test properly the response codes, thanks!
fixtures support
If anyone wants to use fixtures, then WUnitTestCase has to extend CDbTestCase instead of CTestCase.
// wunit/WUnitTestCase.php [php] abstract class WUnitTestCase extends CDbTestCase {
testing model with file
i am trying to use the example for testing file upload, but I am getting the error below. though, I have checked, $picture IS an instance of UploadedFile, at least get_class says that, maybe some namespace collisions?
# Form submission with a file upload use Symfony\HttpFoundation\File\UploadedFile; $photo = new UploadedFile( '/path/to/photo.jpg', 'photo.jpg', 'image/jpeg', 123 ); # or $photo = array( 'tmp_name' => '/path/to/photo.jpg', 'name' => 'photo.jpg', 'type' => 'image/jpeg', 'size' => 123, 'error' => UPLOAD_ERR_OK ); $client->request( 'POST', '/submit', array('name' => 'Chris'), array('photo' => $photo) );
Error:
InvalidArgumentException: An uploaded file must be an array or an instance of UploadedFile. app/protected/extensions/wunit/HttpFoundation/FileBag.php:63
If you have any questions, please ask in the forum instead.
Signup or Login in order to comment.