Yii 1.1: Functional Testing in Yii using Goutte and PHPUnit

18 followers

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.

yiic webapp testdrive

Change directory to newly created project.

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

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

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

phpunit --filter testLoginLogout -c protected/tests/ protected/tests/functional/SiteTest.php

To run all tests

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

Total 2 comments

#8249 report it
Daniel at 2012/05/21 11:27pm
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

#5683 report it
raysto at 2011/11/01 02:17pm
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.

Leave a comment

Please to leave your comment.

Write new article