Test driven development_for_php
 

Test driven development_for_php

on

  • 3,645 views

Training material I used to introduce Test Driven Development to PHP programmers for one of my clients.

Training material I used to introduce Test Driven Development to PHP programmers for one of my clients.

Statistics

Views

Total Views
3,645
Views on SlideShare
3,557
Embed Views
88

Actions

Likes
0
Downloads
17
Comments
0

3 Embeds 88

http://www.pharmarketeer.com 85
http://www.slideshare.net 2
http://www.docshut.com 1

Accessibility

Categories

Upload Details

Uploaded via as Microsoft PowerPoint

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment

Test driven development_for_php Test driven development_for_php Presentation Transcript

  • 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:PHPPEAR;d:phpincludes;d:apacheclasses;d:apacheconf;d:apacheincludes') in D:devwork dd_dev est_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 dd_dev est_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 dd_devTestBasicDataObj.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 dd_devdb.php:14 D:devwork dd_devTestDbObj.php:26 D:devwork dd_devTestDbObj.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!