Functional Testing in Yii using Goutte and PHPUnit ¶
Introduction ¶
This is a short tutorial to get started with functional testing in Yii using Goutte and PHPUnit, without Selenium. Goutte is a screen scraping and web crawling library for PHP. I take a different approach to write functional tests than we've already know from Functional Test which is Selenium based tests. In this tutorial we're gonna be using Goutte as crawler to make a request, submit a form or click on a link, then we create tests based on the response. Functional testing with a web crawler like this is not a replacement to Selenium based tests.
We will use Symfony 2 components for writing tests for Yii project, it's gonna be fun :).
Requirements ¶
- Known to works with Yii 1.1.8.
- Goutte requires PHP 5.3.
- PHPUnit used is version 3.5.15
To make the project simple we will translate generated functional test from default Yii new project as a sample case. Now lets get started.
Steps ¶
Create a new Yii project, called testdrive
.
~~~
[sh]
yiic webapp testdrive
~~~
Change directory to newly created project. ~~~ [sh] cd testdrive ~~~
Install Goutte, copy goutte.phar to protected/extensions/goutte.phar
Require goutte.phar
in protected/tests/bootstrap.php
require_once dirname(dirname(__FILE__)).'/extensions/goutte.phar';
Yii::createWebApplication($config);
Open up protected/tests/functional/SiteTest.php
.
The first test we translate is testIndex()
, update the content,
// from (using Selenium)
public function testIndex() {
$this->open('');
$this->assertTextPresent('Welcome');
}
// to (using Goutte)
public function testIndex() {
$crawler = $this->client->request('GET', $this->open('site/index'));
$this->assertEquals('My Web Application', $crawler->filter('title')->text());
}
The $crawler
is instance of Symfony\Component\DomCrawler\Crawler, result from request created by $this->client
, an object of Goutte\Client
which extends Symfony\Component\BrowserKit\Client.
Note the CTestCase
, it is used instead of WebTestCase (for Selenium based tests).
The public function open()
is a helper method to generate routes based on combination of controller/action
as argument. The $crawler->filter()
is a CSS selector that use CssSelector from Symfony Component (included in goutte.phar).
Here's a more complete SiteTest.php after being updated,
<?php
use Goutte\Client;
class SiteTest extends CTestCase {
protected $client;
public function setUp() {
parent::setUp();
$this->client = new Client();
}
// borrowed from http://www.yiiframework.com/wiki/147/functional-tests-independing-from-your-urlmanager-settings
public function open($route, $params=array()) {
$url = explode('phpunit', Yii::app()->createUrl($route, $params));
return TEST_BASE_URL.$url[1];
}
public function testIndex() {
$crawler = $this->client->request('GET', $this->open('site/index'));
$this->assertEquals('My Web Application', $crawler->filter('title')->text());
}
//
}
Run it with ~~~ [sh] phpunit --filter testIndex -c protected/tests/ protected/tests/functional/SiteTest.php ~~~
Next test is testContact()
// from (using Selenium)
public function testContact() {
$this->open('?r=site/contact');
$this->assertTextPresent('Contact Us');
$this->assertElementPresent('name=ContactForm[name]');
$this->type('name=ContactForm[name]','tester');
$this->type('name=ContactForm[email]','tester@example.com');
$this->type('name=ContactForm[subject]','test subject');
$this->click("//input[@value='Submit']");
$this->assertTextPresent('Body cannot be blank.');
}
// to (using Goutte)
public function testContact() {
$crawler = $this->client->request('GET', $this->open('site/contact'));
$this->assertEquals('My Web Application - Contact Us', $crawler->filter('title')->text());
$form = $crawler->filter('input[type=submit]')->form();
$this->assertEquals('POST', strtoupper($form->getMethod()));
$this->assertTrue($form->has('ContactForm[name]'));
// set some values
$form['ContactForm[name]'] = 'tester';
$form['ContactForm[email]'] = 'tester@example.com';
$form['ContactForm[subject]'] = 'test subject';
// submit the form
$crawler = $this->client->submit($form);
$this->assertEquals('Body cannot be blank.', $crawler->filter('#ContactForm_body_em_')->text());
}
Note the $form->has('ContactForm[name]')
to check if the form has a field named ContactForm[name]
.
Run it with
~~~
[sh]
phpunit --filter testContact -c protected/tests/ protected/tests/functional/SiteTest.php
~~~
Last test is testLoginLogout()
.
// from
$this->open('');
// ensure the user is logged out
if($this->isTextPresent('Logout'))
$this->clickAndWait('link=Logout (demo)');
// to
$crawler = $this->client->request('GET', $this->open('site/index'));
// ensure the user is logged out
$this->assertNotRegExp('/Logout/', $this->client->getResponse()->getContent());
Second part of the function.
// from
// test login process, including validation
$this->clickAndWait('link=Login');
$this->assertElementPresent('name=LoginForm[username]');
$this->type('name=LoginForm[username]','demo');
$this->click("//input[@value='Login']");
$this->assertTextPresent('Password cannot be blank.');
$this->type('name=LoginForm[password]','demo');
$this->clickAndWait("//input[@value='Login']");
$this->assertTextNotPresent('Password cannot be blank.');
$this->assertTextPresent('Logout');
// to
// test login process, including validation
$crawler = $this->client->click($crawler->selectLink('Login')->link());
$form = $crawler->filter('input[type=submit]')->form();
$this->assertTrue($form->has('LoginForm[username]'));
$crawler = $this->client->submit($form);
$this->assertRegExp('/Username cannot be blank./', $this->client->getResponse()->getContent());
$this->assertRegExp('/Password cannot be blank./', $this->client->getResponse()->getContent());
$form['LoginForm[username]'] = 'demo';
$form['LoginForm[password]'] = 'demo';
$crawler = $this->client->submit($form);
$this->assertNotRegExp('/Password cannot be blank./', $this->client->getResponse()->getContent());
$this->assertRegExp('/Logout/', $this->client->getResponse()->getContent());
And the last part.
// from
// test logout process
$this->assertTextNotPresent('Login');
$this->clickAndWait('link=Logout (demo)');
$this->assertTextPresent('Login');
}
}
// to
// test logout process
$this->assertNotRegExp('/Login/', $this->client->getResponse()->getContent());
$crawler = $this->client->click($crawler->selectLink('Logout (demo)')->link());
$this->assertRegExp('/Login/', $this->client->getResponse()->getContent());
}
}
PHPUnit command to run this last test is ~~~ [sh] phpunit --filter testLoginLogout -c protected/tests/ protected/tests/functional/SiteTest.php ~~~
To run all tests ~~~ [sh] phpunit -c protected/tests/ protected/tests/functional/SiteTest.php ~~~
The complete listing is available on Gist. Hope this help someone when writing functional tests.
Resources ¶
- Full source code of this tutorial
- Goutte
- Symfony guide to Testing using BrowserKit, CssSelector and DomCrawler.
- API Documentation of BrowserKit/Client
- API Documentation of DomCrawler/Crawler
Testing
Great article. TDD is where it's at. We made testing the centerpiece of our app built on Yii (zurmo.org). We have over 1000+ unit tests across 8 server configs. We utilize selenium as well to have a nice set of functional tests.
Problem to run test....
When run SiteTest.php I got
SiteTest::testIndex() Use of undefined constant CURLOPT_FOLLOWLOCATION - assumed 'CURLOPT_FOLLOWLOCATION'
can you help me with this?
Thanks,
Daniel
If you have any questions, please ask in the forum instead.
Signup or Login in order to comment.