1. Unit Testing after ZF 1.8
Michelangelo van Dam
ZendCon 2010, Santa Clara, CA (USA)
2. Michelangelo van Dam
• Independent Consultant
• Zend Certified Engineer (ZCE)
• President of PHPBenelux
3. This session
What has changed with ZF 1.8 ?
How do we set up our environment ?
How are we testing controllers ?
How are we testing forms ?
How are we testing models ?
9. Birth of Zend_Application
• bootstrapping an “app”
• works the same for any environment
• resources through methods (no registry)
•- clean separation of tests
unit tests
- controller tests
- integration tests (db, web services, …)
11. Unit Testing
•- smallest functional code snippet (unit)
function or class method
•- aims to challenge logic
proving A + B gives C (and not D)
• helpful for refactoring
• essential for bug fixing (is it really a bug ?)
• TDD results in better code
• higher confidence for developers -> managers
12. Controller Testing
•- tests your (ZF) app
is this url linked to this controller ?
•- detects early errors
on front-end (route/page not found)
- on back-end (database changed, service down, …)
• tests passing back and forth of params
• form validation and filtering
• security testing (XSS, SQL injection, …)
13. Database Testing
•- tests the functionality of your database
referred to as “integration testing”
•- checks functionality
CRUD
- stored procedures
- triggers and constraints
• verifies no mystery data changes happen
- UTF-8 in = UTF-8 out
22. Homepage testing
<?php
// file: tests/application/controllers/IndexControllerTest.php
require_once TEST_PATH . '/ControllerTestCase.php';
class IndexControllerTest extends ControllerTestCase
{
public function testCanWeDisplayOurHomepage()
{
// go to the main page of the web application
$this->dispatch('/');
// check if we don't end up on an error page
$this->assertNotController('error');
$this->assertNotAction('error');
// ok, no error so let's see if we're at our homepage
$this->assertModule('default');
$this->assertController('index');
$this->assertAction('index');
$this->assertResponseCode(200);
}
}
35. Starting simple
<?php
// file: tests/application/controllers/IndexControllerTest.php
require_once TEST_PATH . '/ControllerTestCase.php';
class CommentControllerTest extends ControllerTestCase
{
public function testCanWeDisplayOurForm()
{
// go to the main comment page of the web application
$this->dispatch('/comment');
// check if we don't end up on an error page
$this->assertNotController('error');
$this->assertNotAction('error');
$this->assertModule('default');
$this->assertController('comment');
$this->assertAction('index');
$this->assertResponseCode(200);
$this->assertQueryCount('form', 1);
$this->assertQueryCount('form', 1);
$this->assertQueryCount('input[type="text"]', 2);
}
$this->assertQueryCount('input[type="text"]', 3);
$this->assertQueryCount('textarea', 1);
} $this->assertQueryCount('textarea', 1);
36. GET request = index ?
public function testSubmitFailsWhenNotPost()
{
$this->request->setMethod('get');
$this->dispatch('/comment/send-comment');
$this->assertResponseCode(302);
$this->assertRedirectTo('/comment');
}
37. Can we submit our form ?
public function testCanWeSubmitOurForm()
{
$this->request->setMethod('post')
->setPost(array (
'fullName' => 'Unit Tester',
'emailAddress' => 'test@example.com',
'website' => 'http://www.example.com',
'comment' => 'This is a simple test',
));
$this->dispatch('/comment/send-comment');
$this->assertQueryCount('dt', 1);
$this->assertQueryCount('dd', 1);
$this->assertQueryContentContains('dt#fullName',
'<a href="http://www.example.com">Unit Tester</a>');
$this->assertQueryContentContains('dd#comment', 'This is a simple test');
}
38. All other cases ?
/**
* @dataProvider wrongDataProvider
*/
public function testSubmitFailsWithWrongData($fullName, $emailAddress,
$comment)
{
$this->request->setMethod('post')
->setPost(array (
'fullName' => $fullName,
'emailAddress' => $emailAddress,
'comment' => $comment,
));
$this->dispatch('/comment/send-comment');
$this->assertResponseCode(302);
$this->assertRedirectTo('/comment');
}
39. wrongDataProvider
public function wrongDataProvider()
{
return array (
array ('', '', ''),
array ('~', 'bogus', ''),
array ('', 'test@example.com', 'This is correct text'),
array ('Test User', '', 'This is correct text'),
array ('Test User', 'test@example.com', str_repeat('a', 50001)),
);
}
49. Testing models
• uses core PHPUnit_Framework_TestCase class
• tests your business logic !
• can run independent from other tests
•- model testing !== database testing
model testing tests the logic in your objects
- database testing tests the data storage
50. Model setUp/tearDown
<?php
require_once 'PHPUnit/Framework/TestCase.php';
class Application_Model_GuestbookTest extends PHPUnit_Framework_TestCase
{
protected $_gb;
protected function setUp()
{
parent::setUp();
$this->_gb = new Application_Model_Guestbook();
}
protected function tearDown()
{
$this->_gb = null;
parent::tearDown();
}
…
}
51. Simple tests
public function testGuestBookIsEmptyAtConstruct()
{
$this->assertType('Application_Model_GuestBook', $this->_gb);
$this->assertFalse($this->_gb->hasEntries());
$this->assertSame(0, count($this->_gb->getEntries()));
$this->assertSame(0, count($this->_gb));
}
public function testGuestbookAddsEntry()
{
$entry = new Application_Model_GuestbookEntry();
$entry->setFullName('Test user')
->setEmailAddress('test@example.com')
->setComment('This is a test');
$this->_gb->addEntry($entry);
$this->assertTrue($this->_gb->hasEntries());
$this->assertSame(1, count($this->_gb));
}
52. GuestbookEntry tests
…
public function gbEntryProvider()
{
return array (
array (array (
'fullName' => 'Test User',
'emailAddress' => 'test@example.com',
'website' => 'http://www.example.com',
'comment' => 'This is a test',
'timestamp' => '2010-01-01 00:00:00',
)),
array (array (
'fullName' => 'Test Manager',
'emailAddress' => 'testmanager@example.com',
'website' => 'http://tests.example.com',
'comment' => 'This is another test',
'timestamp' => '2010-01-01 01:00:00',
)),
);
}
/**
* @dataProvider gbEntryProvider
* @param $data
*/
public function testEntryCanBePopulatedAtConstruct($data)
{
$entry = new Application_Model_GuestbookEntry($data);
$this->assertSame($data, $entry->__toArray());
}
…
57. Database Testing
•- integration testing
seeing records are getting updated
- data models behave as expected
- data doesn't change encoding (UTF-8 to Latin1)
• database behaviour testing
- CRUD
- stored procedures
- triggers
- master/slave - cluster
- sharding
58. Caveats
•- database should be reset in a “known state”
no influence from other tests
•- system failures cause the test to fail
connection problems
•- unpredictable data fields or types
auto increment fields
- date fields w/ CURRENT_TIMESTAMP
69. Changing records
public function testNewEntryPopulatesDatabase()
{
$data = $this->gbEntryProvider();
foreach ($data as $row) {
$entry = new Application_Model_GuestbookEntry($row[0]);
$entry->save();
unset ($entry);
}
$ds = new Zend_Test_PHPUnit_Db_DataSet_QueryDataSet(
$this->getConnection()
);
$ds->addTable('gbentry', 'SELECT fullName, emailAddress, website, comment,
timestamp FROM gbentry');
$this->assertDataSetsEqual(
$this->createFlatXmlDataSet(
TEST_PATH . "/_files/addedTwoEntries.xml"),
$ds
);
}
70. Expected resultset
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<gbentry fullName="Test User" emailAddress="test@example.com"
website="http://www.example.com" comment="This is a first test"
timestamp="2010-01-01 00:00:00"/>
<gbentry fullName="Obi Wan Kenobi" emailAddress="obi-wan@jedi-
council.com"
website="http://www.jedi-council.com" comment="May the phporce
be with you"
timestamp="2010-01-01 01:00:00"/>
<gbentry fullName="Test User" emailAddress="test@example.com"
website="http://www.example.com" comment="This is a test"
timestamp="2010-01-01 00:00:00"/>
<gbentry fullName="Test Manager" emailAddress="testmanager@example.com"
website="http://tests.example.com" comment="This is another
test"
timestamp="2010-01-01 01:00:00"/>
</dataset>
76. Desire vs Reality
•- desire
+70% code coverage
- test driven development
- clean separation of tests
•- reality
test what counts first (business logic)
- discover the “unknowns” and test them
- combine unit tests with integration tests
77. Automation
•- using a CI system
continuous running your tests
- reports immediately when failure
- provides extra information
‣ copy/paste detection
‣ mess detection &dependency calculations
‣ lines of code
‣ code coverage
‣ story board and test documentation
‣ …