You are viewing revision #2 of this wiki article.
This version may not be up to date with the latest version.
You may want to view the differences to the latest version.
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.