Functional Testing in Yii using Goutte and PHPUnit

Functional Testing in Yii using Goutte and PHPUnit

  1. Introduction
  2. Requirements
  3. Steps
  4. Resources

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

6 0
17 followers
Viewed: 30 518 times
Version: 1.1
Category: Tutorials
Written by: putera
Last updated by: putera
Created on: Oct 21, 2011
Last updated: 13 years ago
Update Article

Revisions

View all history

Related Articles