Test-Driven Development Francis Fish, Pharmarketeer [email_address] This is distributed under the  Creative Commons Attribution-Share Alike 2.0  licence Download from:  http://www.pharmarketeer.com/tdd.html
What? Nutshell: Write test before developing code Write code that meets the bare minumum for the test Write the next test   http://en.wikipedia.org/wiki/Test-driven_development   http://www.slideshare.net/Skud/test-driven-development-tutorial aka - Behaviour Driven Development BDD
Why do TDD? Shortfalls/stable/maintenance 90% cost of software is maintenance - make maintenance and change easy and low cost Legacy code is untested code (as in repeated automated tests that can be used for regression testing) Stress requirements - find weaknesses and misunderstandings sooner rather than later. Reducing QA, cheaper - stressing specification - NO FUDGING - find interpretation errors early. Common language to talk about tests. Testers can even inspect the tests as part of the delivery.
Example: Specification The function add() should add two numbers
First test case We are going to use PHPUnit 3.4.3.   http://www.phpunit.de   Download from http://pear.phpunit.de/get/   This can be installed using PEAR (see instructions from  http://pear.phpunit.de ).
Let's Fail! test_add.php:  <?php require_once 'PHPUnit/Framework.php' ;require_once('add.php'); class TestOfAdd extends PHPUnit_Framework_TestCase {       }  
We Failed! $ phpunit add_test.php  Fatal error: require_once(): Failed opening required 'add.php' (include_path='D:\PHP;.;D:\PHP\PEAR;d:\php\includes;d:\apache\classes;d:\apache\conf;d:\apache\includes') in D:\devwork\tdd_dev\test_add.php on line 5 This is a contrived example - we haven't created the class yet.
Add the empty class create add.php $ phpunit.bat test_add.php  PHPUnit 3.4.3 by Sebastian Bergmann. F Time: 1 second There was 1 failure: 1) Warning No tests found in class &quot;TestOfAdd&quot;. FAILURES! Tests: 1, Assertions: 0, Failures: 1. Now we've made the point about test first, assume we have a file with an empty class.
Add a test to the test class class TestOfAdd extends PHPUnit_Framework_TestCase {     function testAddAddsNumbers() {         $add = new Add();         $this->assertEquals($add->do_add(1,2),3);     } } Fatal error: Call to undefined method Add::do_add() in D:\devwork\tdd_dev\test_add.php on line 11
Add the method add.php:   class Add {   function do_add($one,$other) {     return $one + $other ;   } } . Time: 0 seconds OK (1 test, 1 assertion) add_test.php OK Test cases run: 1/1, Passes: 1, Failures: 0, Exceptions: 0
Painful, wasn't it? BUT: You know every method has at least one test (albeit maybe naive) You know you can introduce changes without breaking any existing code You write small, well-focussed methods You start from the specification and turn it into code So you know you've met the spec as far as you understood it.
next ... What happens when you don't pass numbers What happens when you don't pass enough arguments Discuss.
Designing a data object We want to be able to write code like this:   $db = new DB(&quot;some config info&quot;);  $client = $db -> getRow(&quot;select first_name, last_name from clients&quot;, Array( 'id' => 6)) ; echo $client->first_name ; This needs a data class that is returned by the database helper. The data class needs to take an array of returned data and respond to method calls that ask for the data.
Data Object specification Instantiate from a name/value map Return attribute values for given name Handle capitalisation of attribute names
Data Object tests Instantiate from a name/value map Return attribute values for given name   require_once('PHPUnit/Framework.php'); require_once('data_obj.php'); class TestBasicDataObj extends PHPUnit_Framework_TestCase {   // Simple case - does it work   function testDataObj() {     $data_obj = new DataObj(Array(&quot;field1&quot; => 1, &quot;field2&quot; => &quot;2&quot;));     $this->assertEquals($data_obj->field1,1);     $this->assertEquals($data_obj->field2,&quot;2&quot;);   } } ?>
data_obj.php <?php class DataObj { } ?> This does nothing at the moment   $ phpunit.bat TestBasicDataObj.php PHPUnit 3.4.3 by Sebastian Bergmann. F Time: 1 second There was 1 failure: 1) TestBasicDataObj::testDataObj Failed asserting that <integer:1> matches expected <null>. D:\devwork\tdd_dev\TestBasicDataObj.php:12 FAILURES!
Add some methods to the class class DataObj {   public $data = Array() ;   function __construct($data) {     foreach ( $data as $key => $value ) {       $this->data[strtolower($key)] = $value ;     }   }   function __get($name) {     $idx = strtolower($name) ;     if ( isset($this->data[$idx]) ) return $this->data[$idx];     return null ;   } } Here we use the &quot;magic methods&quot; to give us a class that responds to what we want.
Discussion - what have we missed? Instantiate from a name/value map Return attribute values for given name Handle capitalisation of attribute names
Test mixed case class TestBasicDataObj extends UnitTestCase {   private $test_init = Array(&quot;field1&quot; => 1, &quot;field2&quot; => &quot;2&quot;);   private $data_obj = null;   // This is the per-test setup   function setUp() {     $this->data_obj = new DataObj($this->test_init);   }   // Simple case - does it work   function testDataObj() {     $this->assertEquals($this->data_obj->field1, 1);     $this->assertEquals($this->data_obj->field2, &quot;2&quot;);   }   // Simple case - does do mixed case   function testDataObjMixedCase() {     $this->assertEquals($this->data_obj->Field1, 1);     $this->assertEquals($this->data_obj->fielD2,&quot;2&quot;);   } } This shows how to set up common data for tests - there is an equivalent teardown method too.
Aside: Meaningful messages $ php test1/data_obj_test2.php data_obj_test2.php OK Test cases run: 1/1, Passes: 4, Failures: 0, Exceptions: 0 Let's make it fail $this->assertEquals($this->data_obj->field1, 99) ... 1) TestBasicDataObj::testDataObj Failed asserting that <integer:99> matches expected <integer:1>. This message isn't very good   $this->assertEquals($this->data_obj->field1 , 99, &quot;Field 1 invalid value&quot; ) ... 1) TestBasicDataObj::testDataObj field 1 invalid value Failed asserting that <integer:99> matches expected <integer:1>. data_obj_test2.php This gives a much better error message, aside from field 1 being a &quot;magic spell&quot; name
Discussion - what have we missed? Instantiate from a name/value map Return attribute values for given name Handle capitalisation of attribute names  Throw exception if asked for invalid attribute
Exceptions Asking for an attribute that isn't there is an error and should raise an exception:    // Validate it gets upset when you ask for an invalid attribute   function testDataObjInvalidAttribute() {     $this->setExpectedException( 'Exception',&quot;Invalid data object attribute&quot; );     $this->data_obj->missing_field ;   } ...    We are expecting an exception with a particular message: ..F Time: 1 second There was 1 failure: 1) TestBasicDataObj::testDataObjInvalidAttribute Expected exception Exception FAILURES! Tests: 3, Assertions: 5, Failures: 1
Fix the code    function __get($name) {       $idx = strtolower($name) ;     if ( isset($this->data[$idx]) ) return $this->data[$idx];     throw new Exception(&quot;Invalid data object attribute&quot; );   } ... PHPUnit 3.4.3 by Sebastian Bergmann. ... Time: 1 second OK (3 tests, 6 assertions) Note that you can expect error messages as well.
Recap: the test class require_once 'PHPUnit/Framework.php' ; require_once('data_obj.php'); class TestBasicDataObj extends PHPUnit_Framework_TestCase {   private $test_init = Array(&quot;field1&quot; => 1, &quot;field2&quot; => &quot;2&quot;);   private $data_obj = null;   // This is the per-test setup   function setUp() {     $this->data_obj = new DataObj($this->test_init);   }   // Simple case - does it work   function testDataObj() {     $this->assertEquals($this->data_obj->field1, 99,&quot;field 1 invalid value&quot;);     $this->assertEquals($this->data_obj->field2, &quot;2&quot;);   }   // Simple case - does do mixed case   function testDataObjMixedCase() {     $this->assertEquals($this->data_obj->Field1, 1);     $this->assertEquals($this->data_obj->fielD2, &quot;2&quot;);   }   function testDataObjInvalidAttribute() {     $this->setExpectedException( 'Exception',&quot;Invalid data object attribute&quot; );     $this->data_obj->missing_field ;   } }
Advanced example - mock and stub  
Mocks and Stubs A Mock - pretends to be a collaborating object and allows you to create responses for that object.  Mocks have  expectations . You can say that you expect a given method to be called as part of the test (and even how many times). Stub - Override a given method or class and return fixed responses. Stubs don't have expectations. Tests assert responses from the object under test are correct given the stub. http://martinfowler.com/articles/mocksArentStubs.html
We don' need no stinkin' datybasey Let's take a step back and think about the class that will be returning the simple data object (or arrays of them, depending). Change parameters into some SQL Get &quot;stuff&quot; Return data object
DB Class &quot;formal&quot; specification getRow() method: Returns a DataObj class Returns a DataObj class with relevant data Returns a DataObj populated from the database output  Sends the correct SQL to the database
Discussion We want this:   $db = new DB(&quot;some config info&quot;);  $client = $db -> getRow(&quot;select first_name, last_name from clients&quot;, Array( 'id' => 6)) ; echo $client->first_name ;   So ... something like this to start:      function testGetRowReturnsDataObj(){     $db = new DB(&quot;something&quot;);     $client = $db -> getRow(&quot;select first_name, last_name from clients&quot;, Array( 'id' => 6)) ;     $this->isInstanceOf($client,DataObj);   } Check we get a DataObj back.
Only do what the test asks <?php require_once('data_obj.php'); class DB {   function getRow($sql,$bind_args = array()){     return new DataObj(array());   } } The test, the test and nothing but the test - we aren't even using the constructor.
DB Class &quot;formal&quot; specification getRow() method: Returns a DataObj class Returns a DataObj class with relevant data Returns a DataObj populated from the database output  Sends the correct SQL to the database
Get some data back    function testGetRowGivesCorrectValues(){     $db = new DB(&quot;something&quot;);     $client = $db -> getRow(&quot;select first_name, last_name from clients&quot;, Array( 'id' => 6)) ;     $this->assertEquals($client->first_name, &quot;some constant we know&quot;, &quot;Client first name not correct&quot;);   } ... There was 1 error: 1) TestDbObj::testGetRowGivesCorrectValues Exception: Invalid data object attribute This is the DataObj complaining about not being initialised with a value.
Change the class class DB {   function getRow($sql,$bind_args = array()){     return new DataObj(array(&quot;first_name&quot; => &quot;some constant we know&quot;));   } } Again the bare minimum
Mock the database Assume that the DB class has a database access object set in the constructor.    Let's create a mock for the existing database access wrapper (OraDB, say) and tidy up the repetition:
require_once 'PHPUnit/Framework.php' ; require_once('db.php'); // Note that the constructor has been hacked because it tries to instantiate a db connection require_once('./Oradb.php'); class TestDbObj extends PHPUnit_Framework_TestCase {    private $db_handler = null ;   private $db = null ;   private $client = null ;   // Set up the tests   function setUp(){     $this->db_handler = $this -> getMock('Oradb');     $this->db = new DB($this->db_handler);   }   // helper    function get_client()   {     $this->client = $this->db -> getRow(&quot;select first_name, last_name from clients&quot;, Array( 'id' => 6)) ;   }   // Naive test of get row function   function testGetRowReturnsDataObj(){     $this->get_client();     $this->isInstanceOf($this->client,DataObj);   }      // ... etc ... }
DB Class &quot;formal&quot; specification getRow() method: Returns a DataObj class Returns a DataObj class with relevant data Returns a DataObj populated from the database output   Sends the correct SQL to the database
Stub database calls // Stub out the return from oSelect - have to create an entirely new stub   function testGetRowHandlesReturnArray(){     $this->db_handler = $this -> getMock('Oradb');     $this->db_handler->expects($this->any())       ->method('oselect')       ->will($this->returnValue(array('first_name' => &quot;some other constant we know&quot;)));     $this->db = new DB($this->db_handler);     $this->get_client();     $this->assertEquals($this->client->first_name, &quot;some other constant we know&quot;, &quot;Client first name not correct&quot;);   }     This is a stub call - just handing back a constant argument - note that the function name is all lower case.
Change the class class DB {   private $db = null ;   function __construct($db)   {     $this->db = $db ;   }   function getRow($sql,$bind_args = array()){     $data = $this->db->oselect($sql,$rval) ;     return new DataObj($data);   } }   Now we are using the oselect method in our code. Note that it expects the correct number of arguments for the stub.
DB Class &quot;formal&quot; specification getRow() method: Returns a DataObj class Returns a DataObj class with relevant data Returns a DataObj populated from the database output  Sends the correct SQL to the database
Check getRow() SQL generation      // Make sure that getRow parses its arguments into some correct-seeming SQL    function testGetRowSendsCorrectSQL(){     $this->db_handler->expects($this->once())       ->method('oselect')       ->with($this->equalTo('select first_name, last_name from clients where id = 6'))       ->will($this->returnValue(array('first_name' => &quot;some other constant we know&quot;)));     $this->db = new DB($this->db_handler);     $this->get_client();   } ... 1) TestDbObj::testGetRowSendsCorrectSQL Failed asserting that two strings are equal. --- Expected +++ Actual @@ @@ -select first_name, last_name from clients where id = 6 +select first_name, last_name from clients D:\devwork\tdd_dev\db.php:14 D:\devwork\tdd_dev\TestDbObj.php:26 D:\devwork\tdd_dev\TestDbObj.php:57
Fix the class require_once('classes/data_obj.php'); class DB {   private $db = null ;   function __construct($db)   {     $this->db = $db ;   }   function getRow($sql,$bind_args = array()){     $query = $sql ;     $delimiter = 'where' ;     foreach ( $bind_args as $key => $value ) {       $query .= &quot; $delimiter $key = $value &quot; ;       $delimiter = 'and' ;     }     $data = $this->db->oselect($query,$rval) ;     return new DataObj($data);   } } ... This fails!! 1) TestDbObj::testGetRowSendsCorrectSQL Failed asserting that two strings are equal. --- Expected +++ Actual @@ @@ -select first_name, last_name from clients where id = 6 +select first_name, last_name from clients where id = 6
Why did it fail? The method in the DB class leaves a trailing space at the end of the string. If you add the trailing space to the expectations it will succeed. This is a very brittle test. If you use a pattern instead it could work, but there seems to be no pattern expectation available as the assert pattern method needs two arguments. This is still a brittle test, and needs some more thought. Discuss.
The results of more thought It would be better to put the creation of the SQL into its own function that can be tested independently. This is one of the ways that TDD drives you to make better decisions about the structure of the code, forcing a change like this will make it more reusable.
Where next? The DB class needs to use bind variables. It will explode if you pass it strings, for example. Developing it more needs to mock out methods like bindstart and bindadd. Note that the way PHPUnit does mocking it tries to call a no-args constructor, in the case of our oradb class it tried to set up a database connection. In order to get these examples to work I made a copy of the class and commented out the body of the constructor. A more sensible way of doing this is to create a static method puts the class in &quot;test mode&quot; that makes the constructor do nothing.
BANG! We Failed!

Test driven development_for_php

  • 1.
    Test-Driven Development FrancisFish, Pharmarketeer [email_address] This is distributed under the  Creative Commons Attribution-Share Alike 2.0  licence Download from:  http://www.pharmarketeer.com/tdd.html
  • 2.
    What? Nutshell: Writetest before developing code Write code that meets the bare minumum for the test Write the next test   http://en.wikipedia.org/wiki/Test-driven_development   http://www.slideshare.net/Skud/test-driven-development-tutorial aka - Behaviour Driven Development BDD
  • 3.
    Why do TDD?Shortfalls/stable/maintenance 90% cost of software is maintenance - make maintenance and change easy and low cost Legacy code is untested code (as in repeated automated tests that can be used for regression testing) Stress requirements - find weaknesses and misunderstandings sooner rather than later. Reducing QA, cheaper - stressing specification - NO FUDGING - find interpretation errors early. Common language to talk about tests. Testers can even inspect the tests as part of the delivery.
  • 4.
    Example: Specification Thefunction add() should add two numbers
  • 5.
    First test caseWe are going to use PHPUnit 3.4.3.   http://www.phpunit.de   Download from http://pear.phpunit.de/get/   This can be installed using PEAR (see instructions from http://pear.phpunit.de ).
  • 6.
    Let's Fail! test_add.php: <?php require_once 'PHPUnit/Framework.php' ;require_once('add.php'); class TestOfAdd extends PHPUnit_Framework_TestCase {       }  
  • 7.
    We Failed! $phpunit add_test.php Fatal error: require_once(): Failed opening required 'add.php' (include_path='D:\PHP;.;D:\PHP\PEAR;d:\php\includes;d:\apache\classes;d:\apache\conf;d:\apache\includes') in D:\devwork\tdd_dev\test_add.php on line 5 This is a contrived example - we haven't created the class yet.
  • 8.
    Add the emptyclass create add.php $ phpunit.bat test_add.php PHPUnit 3.4.3 by Sebastian Bergmann. F Time: 1 second There was 1 failure: 1) Warning No tests found in class &quot;TestOfAdd&quot;. FAILURES! Tests: 1, Assertions: 0, Failures: 1. Now we've made the point about test first, assume we have a file with an empty class.
  • 9.
    Add a testto the test class class TestOfAdd extends PHPUnit_Framework_TestCase {     function testAddAddsNumbers() {         $add = new Add();         $this->assertEquals($add->do_add(1,2),3);     } } Fatal error: Call to undefined method Add::do_add() in D:\devwork\tdd_dev\test_add.php on line 11
  • 10.
    Add the methodadd.php:   class Add {   function do_add($one,$other) {     return $one + $other ;   } } . Time: 0 seconds OK (1 test, 1 assertion) add_test.php OK Test cases run: 1/1, Passes: 1, Failures: 0, Exceptions: 0
  • 11.
    Painful, wasn't it?BUT: You know every method has at least one test (albeit maybe naive) You know you can introduce changes without breaking any existing code You write small, well-focussed methods You start from the specification and turn it into code So you know you've met the spec as far as you understood it.
  • 12.
    next ... Whathappens when you don't pass numbers What happens when you don't pass enough arguments Discuss.
  • 13.
    Designing a dataobject We want to be able to write code like this:   $db = new DB(&quot;some config info&quot;); $client = $db -> getRow(&quot;select first_name, last_name from clients&quot;, Array( 'id' => 6)) ; echo $client->first_name ; This needs a data class that is returned by the database helper. The data class needs to take an array of returned data and respond to method calls that ask for the data.
  • 14.
    Data Object specificationInstantiate from a name/value map Return attribute values for given name Handle capitalisation of attribute names
  • 15.
    Data Object testsInstantiate from a name/value map Return attribute values for given name   require_once('PHPUnit/Framework.php'); require_once('data_obj.php'); class TestBasicDataObj extends PHPUnit_Framework_TestCase {   // Simple case - does it work   function testDataObj() {     $data_obj = new DataObj(Array(&quot;field1&quot; => 1, &quot;field2&quot; => &quot;2&quot;));     $this->assertEquals($data_obj->field1,1);     $this->assertEquals($data_obj->field2,&quot;2&quot;);   } } ?>
  • 16.
    data_obj.php <?php classDataObj { } ?> This does nothing at the moment   $ phpunit.bat TestBasicDataObj.php PHPUnit 3.4.3 by Sebastian Bergmann. F Time: 1 second There was 1 failure: 1) TestBasicDataObj::testDataObj Failed asserting that <integer:1> matches expected <null>. D:\devwork\tdd_dev\TestBasicDataObj.php:12 FAILURES!
  • 17.
    Add some methodsto the class class DataObj {   public $data = Array() ;   function __construct($data) {     foreach ( $data as $key => $value ) {       $this->data[strtolower($key)] = $value ;     }   }   function __get($name) {     $idx = strtolower($name) ;     if ( isset($this->data[$idx]) ) return $this->data[$idx];     return null ;   } } Here we use the &quot;magic methods&quot; to give us a class that responds to what we want.
  • 18.
    Discussion - whathave we missed? Instantiate from a name/value map Return attribute values for given name Handle capitalisation of attribute names
  • 19.
    Test mixed caseclass TestBasicDataObj extends UnitTestCase {   private $test_init = Array(&quot;field1&quot; => 1, &quot;field2&quot; => &quot;2&quot;);   private $data_obj = null;   // This is the per-test setup   function setUp() {     $this->data_obj = new DataObj($this->test_init);   }   // Simple case - does it work   function testDataObj() {     $this->assertEquals($this->data_obj->field1, 1);     $this->assertEquals($this->data_obj->field2, &quot;2&quot;);   }   // Simple case - does do mixed case   function testDataObjMixedCase() {     $this->assertEquals($this->data_obj->Field1, 1);     $this->assertEquals($this->data_obj->fielD2,&quot;2&quot;);   } } This shows how to set up common data for tests - there is an equivalent teardown method too.
  • 20.
    Aside: Meaningful messages$ php test1/data_obj_test2.php data_obj_test2.php OK Test cases run: 1/1, Passes: 4, Failures: 0, Exceptions: 0 Let's make it fail $this->assertEquals($this->data_obj->field1, 99) ... 1) TestBasicDataObj::testDataObj Failed asserting that <integer:99> matches expected <integer:1>. This message isn't very good   $this->assertEquals($this->data_obj->field1 , 99, &quot;Field 1 invalid value&quot; ) ... 1) TestBasicDataObj::testDataObj field 1 invalid value Failed asserting that <integer:99> matches expected <integer:1>. data_obj_test2.php This gives a much better error message, aside from field 1 being a &quot;magic spell&quot; name
  • 21.
    Discussion - whathave we missed? Instantiate from a name/value map Return attribute values for given name Handle capitalisation of attribute names Throw exception if asked for invalid attribute
  • 22.
    Exceptions Asking foran attribute that isn't there is an error and should raise an exception:   // Validate it gets upset when you ask for an invalid attribute   function testDataObjInvalidAttribute() {     $this->setExpectedException( 'Exception',&quot;Invalid data object attribute&quot; );     $this->data_obj->missing_field ;   } ...   We are expecting an exception with a particular message: ..F Time: 1 second There was 1 failure: 1) TestBasicDataObj::testDataObjInvalidAttribute Expected exception Exception FAILURES! Tests: 3, Assertions: 5, Failures: 1
  • 23.
    Fix the code  function __get($name) {      $idx = strtolower($name) ;     if ( isset($this->data[$idx]) ) return $this->data[$idx];     throw new Exception(&quot;Invalid data object attribute&quot; );   } ... PHPUnit 3.4.3 by Sebastian Bergmann. ... Time: 1 second OK (3 tests, 6 assertions) Note that you can expect error messages as well.
  • 24.
    Recap: the testclass require_once 'PHPUnit/Framework.php' ; require_once('data_obj.php'); class TestBasicDataObj extends PHPUnit_Framework_TestCase {   private $test_init = Array(&quot;field1&quot; => 1, &quot;field2&quot; => &quot;2&quot;);   private $data_obj = null;   // This is the per-test setup   function setUp() {     $this->data_obj = new DataObj($this->test_init);   }   // Simple case - does it work   function testDataObj() {     $this->assertEquals($this->data_obj->field1, 99,&quot;field 1 invalid value&quot;);     $this->assertEquals($this->data_obj->field2, &quot;2&quot;);   }   // Simple case - does do mixed case   function testDataObjMixedCase() {     $this->assertEquals($this->data_obj->Field1, 1);     $this->assertEquals($this->data_obj->fielD2, &quot;2&quot;);   }   function testDataObjInvalidAttribute() {     $this->setExpectedException( 'Exception',&quot;Invalid data object attribute&quot; );     $this->data_obj->missing_field ;   } }
  • 25.
    Advanced example -mock and stub  
  • 26.
    Mocks and StubsA Mock - pretends to be a collaborating object and allows you to create responses for that object.  Mocks have expectations . You can say that you expect a given method to be called as part of the test (and even how many times). Stub - Override a given method or class and return fixed responses. Stubs don't have expectations. Tests assert responses from the object under test are correct given the stub. http://martinfowler.com/articles/mocksArentStubs.html
  • 27.
    We don' needno stinkin' datybasey Let's take a step back and think about the class that will be returning the simple data object (or arrays of them, depending). Change parameters into some SQL Get &quot;stuff&quot; Return data object
  • 28.
    DB Class &quot;formal&quot;specification getRow() method: Returns a DataObj class Returns a DataObj class with relevant data Returns a DataObj populated from the database output Sends the correct SQL to the database
  • 29.
    Discussion We wantthis: $db = new DB(&quot;some config info&quot;); $client = $db -> getRow(&quot;select first_name, last_name from clients&quot;, Array( 'id' => 6)) ; echo $client->first_name ;   So ... something like this to start:     function testGetRowReturnsDataObj(){     $db = new DB(&quot;something&quot;);     $client = $db -> getRow(&quot;select first_name, last_name from clients&quot;, Array( 'id' => 6)) ;     $this->isInstanceOf($client,DataObj);   } Check we get a DataObj back.
  • 30.
    Only do whatthe test asks <?php require_once('data_obj.php'); class DB {   function getRow($sql,$bind_args = array()){     return new DataObj(array());   } } The test, the test and nothing but the test - we aren't even using the constructor.
  • 31.
    DB Class &quot;formal&quot;specification getRow() method: Returns a DataObj class Returns a DataObj class with relevant data Returns a DataObj populated from the database output Sends the correct SQL to the database
  • 32.
    Get some databack   function testGetRowGivesCorrectValues(){     $db = new DB(&quot;something&quot;);     $client = $db -> getRow(&quot;select first_name, last_name from clients&quot;, Array( 'id' => 6)) ;     $this->assertEquals($client->first_name, &quot;some constant we know&quot;, &quot;Client first name not correct&quot;);   } ... There was 1 error: 1) TestDbObj::testGetRowGivesCorrectValues Exception: Invalid data object attribute This is the DataObj complaining about not being initialised with a value.
  • 33.
    Change the classclass DB {   function getRow($sql,$bind_args = array()){     return new DataObj(array(&quot;first_name&quot; => &quot;some constant we know&quot;));   } } Again the bare minimum
  • 34.
    Mock the databaseAssume that the DB class has a database access object set in the constructor.    Let's create a mock for the existing database access wrapper (OraDB, say) and tidy up the repetition:
  • 35.
    require_once 'PHPUnit/Framework.php' ;require_once('db.php'); // Note that the constructor has been hacked because it tries to instantiate a db connection require_once('./Oradb.php'); class TestDbObj extends PHPUnit_Framework_TestCase {   private $db_handler = null ;   private $db = null ;   private $client = null ;   // Set up the tests   function setUp(){     $this->db_handler = $this -> getMock('Oradb');     $this->db = new DB($this->db_handler);   }   // helper   function get_client()   {     $this->client = $this->db -> getRow(&quot;select first_name, last_name from clients&quot;, Array( 'id' => 6)) ;   }   // Naive test of get row function   function testGetRowReturnsDataObj(){     $this->get_client();     $this->isInstanceOf($this->client,DataObj);   }     // ... etc ... }
  • 36.
    DB Class &quot;formal&quot;specification getRow() method: Returns a DataObj class Returns a DataObj class with relevant data Returns a DataObj populated from the database output Sends the correct SQL to the database
  • 37.
    Stub database calls// Stub out the return from oSelect - have to create an entirely new stub   function testGetRowHandlesReturnArray(){     $this->db_handler = $this -> getMock('Oradb');     $this->db_handler->expects($this->any())       ->method('oselect')       ->will($this->returnValue(array('first_name' => &quot;some other constant we know&quot;)));     $this->db = new DB($this->db_handler);     $this->get_client();     $this->assertEquals($this->client->first_name, &quot;some other constant we know&quot;, &quot;Client first name not correct&quot;);   }    This is a stub call - just handing back a constant argument - note that the function name is all lower case.
  • 38.
    Change the classclass DB {   private $db = null ;   function __construct($db)   {     $this->db = $db ;   }   function getRow($sql,$bind_args = array()){     $data = $this->db->oselect($sql,$rval) ;     return new DataObj($data);   } }   Now we are using the oselect method in our code. Note that it expects the correct number of arguments for the stub.
  • 39.
    DB Class &quot;formal&quot;specification getRow() method: Returns a DataObj class Returns a DataObj class with relevant data Returns a DataObj populated from the database output Sends the correct SQL to the database
  • 40.
    Check getRow() SQLgeneration     // Make sure that getRow parses its arguments into some correct-seeming SQL   function testGetRowSendsCorrectSQL(){     $this->db_handler->expects($this->once())       ->method('oselect')       ->with($this->equalTo('select first_name, last_name from clients where id = 6'))       ->will($this->returnValue(array('first_name' => &quot;some other constant we know&quot;)));     $this->db = new DB($this->db_handler);     $this->get_client();   } ... 1) TestDbObj::testGetRowSendsCorrectSQL Failed asserting that two strings are equal. --- Expected +++ Actual @@ @@ -select first_name, last_name from clients where id = 6 +select first_name, last_name from clients D:\devwork\tdd_dev\db.php:14 D:\devwork\tdd_dev\TestDbObj.php:26 D:\devwork\tdd_dev\TestDbObj.php:57
  • 41.
    Fix the classrequire_once('classes/data_obj.php'); class DB {   private $db = null ;   function __construct($db)   {     $this->db = $db ;   }   function getRow($sql,$bind_args = array()){     $query = $sql ;     $delimiter = 'where' ;     foreach ( $bind_args as $key => $value ) {       $query .= &quot; $delimiter $key = $value &quot; ;       $delimiter = 'and' ;     }     $data = $this->db->oselect($query,$rval) ;     return new DataObj($data);   } } ... This fails!! 1) TestDbObj::testGetRowSendsCorrectSQL Failed asserting that two strings are equal. --- Expected +++ Actual @@ @@ -select first_name, last_name from clients where id = 6 +select first_name, last_name from clients where id = 6
  • 42.
    Why did itfail? The method in the DB class leaves a trailing space at the end of the string. If you add the trailing space to the expectations it will succeed. This is a very brittle test. If you use a pattern instead it could work, but there seems to be no pattern expectation available as the assert pattern method needs two arguments. This is still a brittle test, and needs some more thought. Discuss.
  • 43.
    The results ofmore thought It would be better to put the creation of the SQL into its own function that can be tested independently. This is one of the ways that TDD drives you to make better decisions about the structure of the code, forcing a change like this will make it more reusable.
  • 44.
    Where next? TheDB class needs to use bind variables. It will explode if you pass it strings, for example. Developing it more needs to mock out methods like bindstart and bindadd. Note that the way PHPUnit does mocking it tries to call a no-args constructor, in the case of our oradb class it tried to set up a database connection. In order to get these examples to work I made a copy of the class and commented out the body of the constructor. A more sensible way of doing this is to create a static method puts the class in &quot;test mode&quot; that makes the constructor do nothing.
  • 45.