Advanced PHPUnit Testing

46
Advanced PHP Unit Testing The 2008 DC PHP Conference June 2nd, 2008

description

Utilizing some of the advanced features of PHPUnit testing. This presentation was given at the 2008 DC PHP conference.

Transcript of Advanced PHPUnit Testing

Page 1: Advanced PHPUnit Testing

AdvancedPHP Unit TestingThe 2008 DC PHP Conference

June 2nd, 2008

Page 2: Advanced PHPUnit Testing

Hello!• Mike Lively

• Lead DeveloperSelling Source, Inc.

• PHPUnit Contributor

• PHPUnit Database Extension

• A frequent inhabitant of #phpc

• http://digitalsandwich.com

Page 3: Advanced PHPUnit Testing

PHPUnit• A widely used unit testing framework

• Created by Sebastian Bergmann

• Originally built as a port of Junit

• Now incredibly feature rich

Page 4: Advanced PHPUnit Testing

PHPUnit - Advanced Features• Mock Objects - A way to further isolate code while testing.

• Database Extension - A DBUnit port to allow creation of fixtures for database content.

• Selenium Integration - A gateway to integration testing.

• PHPUnderControl - Not really PHPUnit...but still very cool.

• Much to cover and not much of time. Let's get started!

Page 5: Advanced PHPUnit Testing

PHPUnit - Mock Objects• Mock Objects - A way to further isolate code while testing.

• Creates 'Fake' objects that mimic 'real' objects in controlled ways.

• Used to ensure that expected methods are being called in expected ways.

• Used to ensure that methods that are depended on by the unit you are testing do not 'pollute' your test with their behavior.

Page 6: Advanced PHPUnit Testing

PHPUnit - Mock Objects• How do I ensure a method is called properly?

Page 7: Advanced PHPUnit Testing

PHPUnit - Mock Objects• How do I ensure a method is called properly?

• Create a mock object!

• PHPUnit_Framework_TestCase::getMock(  string $className,  array $methods = array(),  array $constructParms = array(),  string $mockClassName = '',  bool $callOriginalConstruct = TRUE,  bool $callOriginalClone = TRUE,  bool $callAutoLoad = TRUE);

Page 8: Advanced PHPUnit Testing

PHPUnit - Mock Objects• Create the mock object• <?php

require_once 'PHPUnit/Framework.php'; class ObserverTest extends PHPUnit_Framework_TestCase{    public function testUpdateIsCalledOnce()    {        $observer = $this->getMock('Observer', array('update'));    }}?>

Page 9: Advanced PHPUnit Testing

PHPUnit - Mock Objects• Setup the expectation• <?php

require_once 'PHPUnit/Framework.php'; class ObserverTest extends PHPUnit_Framework_TestCase{    public function testUpdateIsCalledOnce()    {        $observer = $this->getMock('Observer', array('update'));

        $observer->expects($this->once())                 ->method('update')                 ->with($this->equalTo('something'));    }}?>

Page 10: Advanced PHPUnit Testing

PHPUnit - Mock Objects• expects() - Sets how many times you expect a method to

be called:o any()o never()o atLeastOnce()o once()o exactly($count)o at($index)

• method() - The name of the method you are setting the expectation for

Page 11: Advanced PHPUnit Testing

PHPUnit - Mock Objects• with() - Each parameter is validated using PHPUnit

Constraints:o anything()o contains($value)o arrayHasKey($key)o equalTo($value, $delta, $maxDepth)o classHasAttribute($attribute)o greaterThan($value)o isInstanceOf($className)o isType($type)o matchesRegularExpression($regex)o stringContains($string, $case)

• withAnyParameters() - A quick way to say "I don't care"

Page 12: Advanced PHPUnit Testing

PHPUnit - Mock Objects• Call the tested code• <?php

require_once 'PHPUnit/Framework.php'; class ObserverTest extends PHPUnit_Framework_TestCase{    public function testUpdateIsCalledOnce()    {        $observer = $this->getMock('Observer', array('update'));

        $observer->expects($this->once())                 ->method('update')                 ->with($this->equalTo('something'));

        $subject = new Subject;        $subject->attach($observer);         $subject->doSomething();    }}?>

Page 13: Advanced PHPUnit Testing

PHPUnit - Mock Objects• Force methods not being tested to return particular values.

• will() - Force the current method to return a particular valueo returnValue($value) - Returns the specified valueo throwException($exception) - Throws the specified

exception objecto onConsecutiveCalls(...) - Performs each action (return

or exception) consecutively.

Page 14: Advanced PHPUnit Testing

PHPUnit - Mock Objects• Forcing methods to return certain values• <?php

require_once 'PHPUnit/Framework.php'; class StubTest extends PHPUnit_Framework_TestCase{    public function testStub()    {        $stub = $this->getMock('SomeClass', array('doSomething'));        $stub->expects($this->any())             ->method('doSomething')             ->will($this->returnValue('foo'));         // Calling $stub->doSomething() will now return 'foo'.    }}?>

Page 15: Advanced PHPUnit Testing

PHPUnit - Database ExtensionThe PHPUnit Database Extension

Page 16: Advanced PHPUnit Testing

PHPUnit - Database Extension• PHPUnit Database Extension - DBUnit Port

• Uses many of the same concepts

• Dataset formats are essentially identical

• Puts your database into a known state prior to testing

• Allows for setting expectations of data in your database at the end of a test

Page 17: Advanced PHPUnit Testing

PHPUnit - Database Extension• PHPUnit_Extensions_Database_TestCase

• Overloads setUp() and tearDown() and introduces four new overridable methods (2 MUST be overriden)

• If implementing setUp or tearDown, please be sure to call parent::setUp() and parent::tearDown()

Page 18: Advanced PHPUnit Testing

PHPUnit - Database Extension• Must implement

o getConnection() - Returns a database connection wrapper

o getDataSet() - Returns the dataset to seed the test database with.

• Can overrideo getSetUpOperation() - Returns the operation used to set

up the database (defaults to CLEAN_INSERT)

o getTearDownOperation() - Returns the operation to used to tear down the database (defaults to NONE)

Page 19: Advanced PHPUnit Testing

PHPUnit - Database Extension• getConnection() returns a customized wrapper around a

database connection.

• Current implementation uses PDO

• This does not mean code you are testing has to use PDO

• createDefaultDBConnection(  PDO $pdo,   string $schema); // Convenience method

• Very important to provide the schema name.

Page 20: Advanced PHPUnit Testing

PHPUnit - Database Extension• Setting up your database test case connection• <?php

require_once 'PHPUnit/Extensions/Database/TestCase.php'; class DatabaseTest extends PHPUnit_Extensions_Database_TestCase{    protected function getConnection()    {        $pdo = new PDO('mysql:host=localhost;dbname=testdb', 'root', '');        return $this->createDefaultDBConnection($pdo, 'testdb');    }}?>

Page 21: Advanced PHPUnit Testing

PHPUnit - Database Extension• Currently (known) supported databases:

o MySQLo SQLiteo Postgreso OCI8

• Need another RDBMS supported? I NEED YOU!!!

Page 22: Advanced PHPUnit Testing

PHPUnit - Database Extension• getDataSet() returns one of the following types of data

sets:o Flat XMLo XMLo Databaseo Default (php array basically)

• Can be a pain to create

• Working on utilities to make it...less painful

Page 23: Advanced PHPUnit Testing

PHPUnit - Database Extension• Convenience methods for datasets

• $this->createFlatXMLDataSet($xmlFile);

• $this->createXMLDataSet($xmlFile);

• Use full paths

Page 24: Advanced PHPUnit Testing

PHPUnit - Database Extension• Setting up your database test case's common fixture• <?php

require_once 'PHPUnit/Extensions/Database/TestCase.php'; class DatabaseTest extends PHPUnit_Extensions_Database_TestCase{    protected function getConnection()    {        $pdo = new PDO('mysql:host=localhost;dbname=testdb', 'root', '');        return $this->createDefaultDBConnection($pdo, 'testdb');    }     protected function getDataSet()    {        return $this->createFlatXMLDataSet(dirname(__FILE__).'/_files/bank-account-seed.xml');    }}?>

Page 25: Advanced PHPUnit Testing

PHPUnit - Flat XML Data Set• Each element represents a row, each attribute represents

a column.

• All attributes must be present in the first element for a given table.

• Specifying an element with no attributes represents an empty table.

• If an attribute specified in the first element is missing in subsequent element, NULL is implied.

• There is no explicit NULL

Page 26: Advanced PHPUnit Testing

PHPUnit - Database Extension• Flat XML• <dataset>

     <TEST_TABLE COL0="row 0 col 0"          COL1="row 0 col 1"         COL2="row 0 col 2"/>

     <TEST_TABLE COL1="row 1 col 1"/>

     <SECOND_TABLE COL0="row 0 col 0"          COL1="row 0 col 1" />

     <EMPTY_TABLE/></dataset>

Page 27: Advanced PHPUnit Testing

PHPUnit - XML Data Set• A more robust and capable format

• <table> element for each table

• <column> element for each column

• <row> element for each row

• <row> will contain equal number of children as there are <column>s in the same order.

• <value>val</value> is used to specify a value

• <null/> used to explicitly specify NULL

Page 28: Advanced PHPUnit Testing

PHPUnit - Database Extension• XML (Not so flat)• <dataset>

    <table name="TEST_TABLE">        <column>COL0</column>        <column>COL1</column>        <column>COL2</column>

        <row>            <value>row 0 col 0</value>            <value>row 0 col 1</value>            <value>row 0 col 2</value>        </row>

        <row>            <null/>            <value>row 1 col 1</value>            <null/>        </row>    </table>

    <table name='EMPTY_TABLE'>        <column>COLUMN0</column>        <column>COLUMN1</column>    </table>

</dataset>

Page 29: Advanced PHPUnit Testing

PHPUnit - Other Data Sets• Pull your data sets out of a database

o Possible, and might be a good way to do it for smaller database test suites.

o Wouldn't recommend it for large ones (yet)

• What about the Default data set?o I haven't hated myself enough to bother with it yet :P

• Future formats? CSV, Composite Data Sets, Query Sets

Page 30: Advanced PHPUnit Testing

PHPUnit - Other Data Sets• Pull your data sets out of a database

o Possible, and might be a good way to do it for smaller database test suites.

o Wouldn't recommend it for large ones (yet)

• What about the Default data set?o I haven't hated myself enough to bother with it yet :P

• Future formats? CSV, Composite Data Sets, Query Sets <- this is when you'll want to use database data sets for large suites

Page 31: Advanced PHPUnit Testing

PHPUnit - OperationsIf you want to specify non-default operations override either getSetUpOperation() or getTearDownOperation() with a static method from the class: PHPUnit_Extensions_Database_Operation_Factory • NONE• CLEAN_INSERT• INSERT• TRUNCATE• DELETE• DELETE_ALL• UPDATE

Page 32: Advanced PHPUnit Testing

PHPUnit - Operations• <?php

require_once 'PHPUnit/Extensions/Database/TestCase.php'; class DatabaseTest extends PHPUnit_Extensions_Database_TestCase{    protected function getSetUpOperation()    {        return PHPUnit_Extensions_Database_Operation_Factory::CLEAN_INSERT();    }     protected function getTearDownOperation()    {        return PHPUnit_Extensions_Database_Operation_Factory::NONE();    }}?>

Page 33: Advanced PHPUnit Testing

PHPUnit - Database Extension• Setting up database expectations.

• Retrieve database contents:o $this->getConnection()->createDataSet();o Creates a dataset with ALL data in your database.o Pass an array of table names if you only want to

compare particular tables.

• You can also use the data set filter if necessary

Page 34: Advanced PHPUnit Testing

PHPUnit - Database Extension• Why Filtered Data Sets? auto increment, time stamps

• PHPUnit_Extensions_Database_DataSet_DataSetFilter decoratoro Accepts existing dataset object as first parameter

o Accepts associative array as second parameter The array indexes are table names

The array values are arrays of column names to exclude from the data set or the string '*' if the whole table is to be excluded.

Page 35: Advanced PHPUnit Testing

PHPUnit - Database Extension• Refreshing our memory• <?php

require_once 'PHPUnit/Extensions/Database/TestCase.php'; class DatabaseTest extends PHPUnit_Extensions_Database_TestCase{    protected function getConnection()    {        $pdo = new PDO('mysql:host=localhost;dbname=testdb', 'root', '');        return $this->createDefaultDBConnection($pdo, 'testdb');    }     protected function getDataSet()    {        return $this->createFlatXMLDataSet(dirname(__FILE__).'/_files/bank-account-seed.xml');    }}?>

Page 36: Advanced PHPUnit Testing

PHPUnit - Database Extension• Writing a test• <?php

require_once 'PHPUnit/Extensions/Database/TestCase.php'; class DatabaseTest extends PHPUnit_Extensions_Database_TestCase{    // ...    public function testAccountBalanceDeposits()    {        $bank_account = new BankAccount('15934903649620486', $this->pdo);        $bank_account->depositMoney(100);            $bank_account = new BankAccount('15936487230215067', $this->pdo);        $bank_account->depositMoney(230);                $bank_account = new BankAccount('12348612357236185', $this->pdo);        $bank_account->depositMoney(24);                 $xml_dataset = $this->createFlatXMLDataSet(dirname(__FILE__).'/_files/bank-account-after-deposits.xml');        $database_dataset = $this->getConnection()->createDataSet(array('bank_account'));    }     // ...}?>

Page 37: Advanced PHPUnit Testing

PHPUnit - Database Extension• Filtering out date_created• <?php

require_once 'PHPUnit/Extensions/Database/TestCase.php'; class DatabaseTest extends PHPUnit_Extensions_Database_TestCase{    // ...    public function testAccountBalanceDeposits()    {        // ...                $xml_dataset = $this->createFlatXMLDataSet(dirname(__FILE__).'/_files/bank-account-after-deposits.xml'); $database_dataset = new PHPUnit_Extensions_Database_DataSet_DataSetFilter( $this->getConnection()->createDataSet(array('bank_account')), array('bank_account' => array('date_created')) ); }     // ...}?>

Page 38: Advanced PHPUnit Testing

PHPUnit - Database Extension• $this->assertDataSetsEqual($expected, $actual)

• Compares all tables in the two data sets

• Must have equal and matching tables with equal and matching data

• Have I mentioned filters?

• $this->assertTablesEqual($expected, $actual)

• There is also a table filter!

Page 39: Advanced PHPUnit Testing

PHPUnit - Database Extension• Writing a test• <?php

require_once 'PHPUnit/Extensions/Database/TestCase.php'; class DatabaseTest extends PHPUnit_Extensions_Database_TestCase{    // ...    public function testAccountBalanceDeposits()    {        $bank_account = new BankAccount('15934903649620486', $this->pdo);        $bank_account->depositMoney(100);            $bank_account = new BankAccount('15936487230215067', $this->pdo);        $bank_account->depositMoney(230);                $bank_account = new BankAccount('12348612357236185', $this->pdo);        $bank_account->depositMoney(24);                 $xml_dataset = $this->createFlatXMLDataSet(dirname(__FILE__) . '/_files/bank-account-after-deposits.xml'); $this->assertDataSetsEqual( $xml_dataset,          $this->getConnection()->createDataSet(array('bank_account')) );    }     // ...}?>

Page 40: Advanced PHPUnit Testing

PHPUnit - Selenium• Selenium is a testing framework for web applications.

• Runs tests by controlling a remote browser session

• Can inspect the dom, look for events, and much much more

• Selenium tests can be made a part of your PHPUnit Test Suite

Page 41: Advanced PHPUnit Testing

PHPUnit - Selenium• Selenium is a testing framework for web applications.

• Runs tests by controlling a remote browser session

• Can inspect the dom, look for events, and much much more

• Selenium tests can be made a part of your PHPUnit Test Suite

Page 42: Advanced PHPUnit Testing

PHPUnit - Selenium

Demo Time!

Page 43: Advanced PHPUnit Testing

PHPUnit - Continuous IntegrationContinuous Integration is a software development practice where members of a team integrate their work frequently, usually each person integrates at least daily, leading to multiple integrations per day. Each integration is verified by an automated build (including test) to detect integration errors as quickly as possible. Many teams find that this approach leads to significantly reduced integration problems and allows a team to develop cohesive software more rapidly.

-- Martin Fowler

Page 44: Advanced PHPUnit Testing

PHPUnit - PHPUnderControl• Created and maintained by Manuel Pischler

• Cruise Control for the php developer

• Runs your Unit Tests

• Builds your documentation

• Lint checks your code

• Gives you build and performance metrics

• The best thing since sliced bread

Page 45: Advanced PHPUnit Testing

PHPUnit - PHPUnderControl

Demo Time!

Page 46: Advanced PHPUnit Testing

Thank you - Resources• PHPUnit: http://phpun.it, http://planet.phpunit.de

• Selenium: http://selenium.openqa.org/

• PHPUnderControl: http://www.phpundercontrol.org/

• My Site: http://www.digitalsandwich.com

• My Employer's Site: http://dev.sellingsource.com

Questions??