An Introduction to the Art of Unit Testing in PHP
Tags: web-development, unit testing
Введение в искусство модульного тестирования в PHP
Введення в мистецтво модульного тестування в PHP
| ← How Google Really Wants You to Optimize Your Site | Integrating FCKeditor with Zend_Form → |
Introduction
Testing is an essential aspect of developing in any programming language. If you don't test your source code then how can you verify it works as expected? Manual testing can only be performed irregularly and usually only in limited ways. The answer to testing source code regularly, and in depth, is to write automated tests which can be frequently executed. In PHP such tests are usually written using a unit testing framework, a framework which allows the source code of any application or library to be tested as isolated units of functionality such as a single class or method. As unit testing has gained popularity, it has become a standard practice in PHP with libraries and frameworks such as Swiftmailer, the Zend Framework and Symfony all requiring unit test coverage of their source code.
Unit Testing is often seen as an arcane, time consuming task - which it sometimes can be! But the point of spending time writing tests is to improve the quality of your source code so it has fewer overall bugs, many of which are detected early, a continual testing process to prevent new changes from changing the behaviour of older code, and to provide confidence that your code can be depended on. There are other benefits too, and we'll detail these later.
The Testing Fallacies
Unit Testing, and actually all other forms of testing, fall afoul of four common excuses which hinder adoption by developers.
1. It's time consuming and takes too long.
2. Complex code cannot be tested.
3. So long as it works, I don't need to write tests.
4. Testing is boring.
These are testing fallacies, excuses which appear quite reasonable but are actually misinformed in subtle ways. So let's clear up a few things!
Testing does take time. The question is why should that time be considered worthwhile and the answer is that it reduces the future time you would consume in modifying code, maintainance, refactoring and fixing undetected bugs. And we both know there would be tons of undetected bugs if you're not testing comprehensively!
Testing early is like catching the proverbial worm; as you write code, you can use Unit Tests to test isolated methods/classes or groups of functional classes immediately. By doing so you find and fix bugs quickly as they are created. A problem is that you find bugs so often, and fix them so quickly, that you barely notice the time it took. When you make changes later, and a test fails, you can fix the integration problem just as quickly - more saved time you barely notice. The benefits can be so well disguised you may miss them completely - and only see the test code it took you a few hours to write, not the bugs you solved in 10 seconds that would have taken minutes or hours six months down the line.
Secondly, there's complex code - and there's complex code made up of smaller practical parts. In OOP a simple objective is often "being testable". It's like a litmus test for quality decoupled code. If your code can't be easily tested than it's not that Unit Testing has failed, it's that you failed to write practical code which was flexible and decoupled. If you were testing as you wrote code, you would have been forced to decouple classes almost on automatic. The fact code seems too complex to test is usually a symptom of having waited too long to start testing!
Thirdly, working code and working tested code are two different animals. Tests offer a safety net which makes changes, refactoring, and new features less of a pain to add since integration issues will be detected almost immediately. They also improve the efficiency of new programmers on your team who, unfamiliar with the code, see their mistakes detected immediately by the shorter feedback loop and so gain experience on the run.
In both cases, if new code changes the behaviour of older code the tests will throw a tantrum. Without tests, some behaviour could be subtly altered without anyone noticing (afterall if you're not testing how are you ever going to detect it until some end-user figures it out in the future and complains!). Don't pat yourself on the back because something works without tests - wait in line at your public bug tracker and wait for the predictable onslaught. If you are very lucky, the code will be simple enough that eventually the bug reports will uncover most of the problems and you can fix them before anyone gets too annoyed. That still does little for your reputation nor the future effect of even more untested changes.
Lastly, testing is not always boring. Oh, I know it can be boring at times but usually it's boring because you're writing tests at the end of the development process. If you're constantly switching from tests to code, and back, you'll get more done with less yawning. So the cure to boredom is simple task switching and timely test writing - don't fall into the trap of having to spend days writing tests and only tests.
So no more excuses. Sit, relax, read on.
Unit Testing in PHP
Unit Testing in PHP can be performed using one of three methods. The two common options are Marcus Baker's SimpleTest and Sebastian Bergmann's PHPUnit. To add some confusion there's also the old reliable: phpt. All three allow you to unit test code, and the two Framework libraries also offer numerous extensions.
We'll take an example with PHPUnit. You'll need a working PEAR installation.
PHPUnit (quite like SimpleTest) organises tests into cases; basically a class whose public methods are singular independent tests. Here we're about to create a class for representing a fleet of ships (a simple Model). Seems overly simple, but let's crawl before we start peg legging it. Here's a test case I've written to get this class up to some minimal level before we even consider a database input.
Hide code highlighting
The test case above is a good small start. We've defined a setUp() and tearDown() pair of methods. In both SimpleTest and PHPUnit, these are used to create Fixtures. A Fixture is some resource all tests in a test case have in common and which sets the context of the test (the situation in which we're testing), as well as other repetitive objects or resources. Because each test must be isolated (cannot share information) these methods will tell the Framework to create our Fixture (a My_Fleet instance) before each test, and destroy it after. So each test gets a shiny new version to play with, free of anything a previous test may have done.
Each test above is a public method whose name starts with "test". Anything not starting with "test" will not be reported on (unless annotated as a test under PHPUnit). You can use this to create Helper methods for setting up a test without fear it will be interpreted as a separate test by the framework. Helper methods are quite handy for repetitive tasks - yes, even tests may be refactored. The class name itself ends, by convention, with "Test".
Inside each test we have references to "assert" methods. An assertion is basically taking an expected value and comparing it to an actual value. If the expected value does not match the actual value, then the test will fail. Knowing what to assert about in your tests is pretty much 90% of the battle. The other 10% is ensuring each test is totally isolated from any other test (using Fixtures and Mock Objects are the main tools for this). I only used assertEquals() above, but a full list for PHPUnit is available from here.
Now we will not present Mock Objects here but they are a hugely important component in maintaining a unit test's isolation from all other classes and resources except the one under test.
Running The Tests
Running PHPUnit Tests can be done from the console (even MS-DOS on Windows Vista) or a web browser. To keep this simple, we'll rely on a quick console command executed from where we store this test class. PHPUnit also describes a method of organising tests into suites which is not covered here but is quite essential unless you intend running all tests one by one for eternity, and the PHPUnit Pocket Guide is a good place to read up on Test Suites and other elements of Unit Testing.
To run our single test class, open your console and navigate to where the test is stored. The console command is a simple:
phpunit MyFleetTest
The test name is assumed to be reflected in the file name saved, so a MyFleetTest class would be stored to the MyFleetTest.php file. Go run the test now. I'll forewarn you that it will of course fail miserably since we haven't gotten around to showing the class code yet! But here it comes...
Unit Testing and Test-Driven Development
You may have noticed that small problem by now. We wrote some tests, but where's the actual class being tested? It's safe to say we could have written the class and then tested it afterwards (almost the normal practice in PHP) but to add a broader perspective I haven't done that.
One popular use for Unit Testing is to practice Test-Driven Development (TDD). TDD is not a necessary component of Unit Testing, rather it's an additional practice which utilises Unit Testing since it's an available standard for writing executable examples. The idea behind TDD is that by describing the behaviour of a class in executable code (i.e. the tests) before coding, it provides a tool for driving how the class should be designed. This has given rise to the battle-cry "Test First". And also to a population of "Test Infected". They should make a horror movie one day.
Consider the tests we just wrote. In writing the tests before we wrote the class, we have been forced to do a few things. First of all, we had to invent the class's public API before anything else. It's worth noting than in Unit Testing and TDD, it is virtually pointless to test private methods (what's called "Testing State") since we're rarely interested in how the code does something as opposed to what the expected end result should be (focus on results, not intermediate state). In a lot of code the result will remain the same, but the working code will evolve over time due to refactoring, new PHP functionality, or system dependent requirements - so testing all this stuff only forces us to constantly rewrite our tests for no good reason.
Now by defining the public API so early we find ourselves designing. We're not actually testing per se (there's no code yet!), instead we're specifying our expectations of how the class WILL act, and then writing the code so it DOES act as we specified.
Once we get down to actual coding, we have all this design work already done and a set of tests which will continually verify the new code we write. So while TDD is a design methodology it still ends up providing a nice set of tests. So let's get our class written.
Hide code highlighting
A working class, and our tests will all pass! Maybe later we'll change the tests to allow for Ship objects which implement __toString() to print the ship name. A small change to the tests to strval() the getShip() return value and our tests would be updated for this changed behaviour.
Original: An Introduction to the Art of Unit Testing in PHP
Testing is an essential aspect of developing in any programming language. If you don't test your source code then how can you verify it works as expected? Manual testing can only be performed irregularly and usually only in limited ways. The answer to testing source code regularly, and in depth, is to write automated tests which can be frequently executed. In PHP such tests are usually written using a unit testing framework, a framework which allows the source code of any application or library to be tested as isolated units of functionality such as a single class or method. As unit testing has gained popularity, it has become a standard practice in PHP with libraries and frameworks such as Swiftmailer, the Zend Framework and Symfony all requiring unit test coverage of their source code.
Unit Testing is often seen as an arcane, time consuming task - which it sometimes can be! But the point of spending time writing tests is to improve the quality of your source code so it has fewer overall bugs, many of which are detected early, a continual testing process to prevent new changes from changing the behaviour of older code, and to provide confidence that your code can be depended on. There are other benefits too, and we'll detail these later.
The Testing Fallacies
Unit Testing, and actually all other forms of testing, fall afoul of four common excuses which hinder adoption by developers.
1. It's time consuming and takes too long.
2. Complex code cannot be tested.
3. So long as it works, I don't need to write tests.
4. Testing is boring.
These are testing fallacies, excuses which appear quite reasonable but are actually misinformed in subtle ways. So let's clear up a few things!
Testing does take time. The question is why should that time be considered worthwhile and the answer is that it reduces the future time you would consume in modifying code, maintainance, refactoring and fixing undetected bugs. And we both know there would be tons of undetected bugs if you're not testing comprehensively!
Testing early is like catching the proverbial worm; as you write code, you can use Unit Tests to test isolated methods/classes or groups of functional classes immediately. By doing so you find and fix bugs quickly as they are created. A problem is that you find bugs so often, and fix them so quickly, that you barely notice the time it took. When you make changes later, and a test fails, you can fix the integration problem just as quickly - more saved time you barely notice. The benefits can be so well disguised you may miss them completely - and only see the test code it took you a few hours to write, not the bugs you solved in 10 seconds that would have taken minutes or hours six months down the line.
Secondly, there's complex code - and there's complex code made up of smaller practical parts. In OOP a simple objective is often "being testable". It's like a litmus test for quality decoupled code. If your code can't be easily tested than it's not that Unit Testing has failed, it's that you failed to write practical code which was flexible and decoupled. If you were testing as you wrote code, you would have been forced to decouple classes almost on automatic. The fact code seems too complex to test is usually a symptom of having waited too long to start testing!
Thirdly, working code and working tested code are two different animals. Tests offer a safety net which makes changes, refactoring, and new features less of a pain to add since integration issues will be detected almost immediately. They also improve the efficiency of new programmers on your team who, unfamiliar with the code, see their mistakes detected immediately by the shorter feedback loop and so gain experience on the run.
In both cases, if new code changes the behaviour of older code the tests will throw a tantrum. Without tests, some behaviour could be subtly altered without anyone noticing (afterall if you're not testing how are you ever going to detect it until some end-user figures it out in the future and complains!). Don't pat yourself on the back because something works without tests - wait in line at your public bug tracker and wait for the predictable onslaught. If you are very lucky, the code will be simple enough that eventually the bug reports will uncover most of the problems and you can fix them before anyone gets too annoyed. That still does little for your reputation nor the future effect of even more untested changes.
Lastly, testing is not always boring. Oh, I know it can be boring at times but usually it's boring because you're writing tests at the end of the development process. If you're constantly switching from tests to code, and back, you'll get more done with less yawning. So the cure to boredom is simple task switching and timely test writing - don't fall into the trap of having to spend days writing tests and only tests.
So no more excuses. Sit, relax, read on.
Unit Testing in PHP
Unit Testing in PHP can be performed using one of three methods. The two common options are Marcus Baker's SimpleTest and Sebastian Bergmann's PHPUnit. To add some confusion there's also the old reliable: phpt. All three allow you to unit test code, and the two Framework libraries also offer numerous extensions.
We'll take an example with PHPUnit. You'll need a working PEAR installation.
PHPUnit (quite like SimpleTest) organises tests into cases; basically a class whose public methods are singular independent tests. Here we're about to create a class for representing a fleet of ships (a simple Model). Seems overly simple, but let's crawl before we start peg legging it. Here's a test case I've written to get this class up to some minimal level before we even consider a database input.
Hide code highlighting
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | require_once 'PHPUnit/Framework.php'; require_once 'My/Fleet.php'; class MyFleetTest extends PHPUnit_Framework_TestCase { protected $_fleet = null; public function setUp() { $this->_fleet = new My_Fleet; } public function tearDown() { unset($this->_fleet); } public function testShouldNotHaveAnyShipsYetInIntitialState() { /** * $this->_fleet->count() is boring; let's implement SPL's Countable interface */ $this->assertEquals(0, count($this->_fleet)); } public function testAddingAShipWillIncrementCountByOne() { $this->_fleet->addShip('USS Enterprise'); $this->assertEquals(1, count($this->_fleet)); } public function testAfterAddingAShipWeCanRetrieveItsNameByIndex() { $this->_fleet->addShip('USS Enterprise'); $this->assertEquals('USS Enterprise', $this->_fleet->getShip( count($this->_fleet) - 1 )); } } |
The test case above is a good small start. We've defined a setUp() and tearDown() pair of methods. In both SimpleTest and PHPUnit, these are used to create Fixtures. A Fixture is some resource all tests in a test case have in common and which sets the context of the test (the situation in which we're testing), as well as other repetitive objects or resources. Because each test must be isolated (cannot share information) these methods will tell the Framework to create our Fixture (a My_Fleet instance) before each test, and destroy it after. So each test gets a shiny new version to play with, free of anything a previous test may have done.
Each test above is a public method whose name starts with "test". Anything not starting with "test" will not be reported on (unless annotated as a test under PHPUnit). You can use this to create Helper methods for setting up a test without fear it will be interpreted as a separate test by the framework. Helper methods are quite handy for repetitive tasks - yes, even tests may be refactored. The class name itself ends, by convention, with "Test".
Inside each test we have references to "assert" methods. An assertion is basically taking an expected value and comparing it to an actual value. If the expected value does not match the actual value, then the test will fail. Knowing what to assert about in your tests is pretty much 90% of the battle. The other 10% is ensuring each test is totally isolated from any other test (using Fixtures and Mock Objects are the main tools for this). I only used assertEquals() above, but a full list for PHPUnit is available from here.
Now we will not present Mock Objects here but they are a hugely important component in maintaining a unit test's isolation from all other classes and resources except the one under test.
Running The Tests
Running PHPUnit Tests can be done from the console (even MS-DOS on Windows Vista) or a web browser. To keep this simple, we'll rely on a quick console command executed from where we store this test class. PHPUnit also describes a method of organising tests into suites which is not covered here but is quite essential unless you intend running all tests one by one for eternity, and the PHPUnit Pocket Guide is a good place to read up on Test Suites and other elements of Unit Testing.
To run our single test class, open your console and navigate to where the test is stored. The console command is a simple:
phpunit MyFleetTest
The test name is assumed to be reflected in the file name saved, so a MyFleetTest class would be stored to the MyFleetTest.php file. Go run the test now. I'll forewarn you that it will of course fail miserably since we haven't gotten around to showing the class code yet! But here it comes...
Unit Testing and Test-Driven Development
You may have noticed that small problem by now. We wrote some tests, but where's the actual class being tested? It's safe to say we could have written the class and then tested it afterwards (almost the normal practice in PHP) but to add a broader perspective I haven't done that.
One popular use for Unit Testing is to practice Test-Driven Development (TDD). TDD is not a necessary component of Unit Testing, rather it's an additional practice which utilises Unit Testing since it's an available standard for writing executable examples. The idea behind TDD is that by describing the behaviour of a class in executable code (i.e. the tests) before coding, it provides a tool for driving how the class should be designed. This has given rise to the battle-cry "Test First". And also to a population of "Test Infected". They should make a horror movie one day.
Consider the tests we just wrote. In writing the tests before we wrote the class, we have been forced to do a few things. First of all, we had to invent the class's public API before anything else. It's worth noting than in Unit Testing and TDD, it is virtually pointless to test private methods (what's called "Testing State") since we're rarely interested in how the code does something as opposed to what the expected end result should be (focus on results, not intermediate state). In a lot of code the result will remain the same, but the working code will evolve over time due to refactoring, new PHP functionality, or system dependent requirements - so testing all this stuff only forces us to constantly rewrite our tests for no good reason.
Now by defining the public API so early we find ourselves designing. We're not actually testing per se (there's no code yet!), instead we're specifying our expectations of how the class WILL act, and then writing the code so it DOES act as we specified.
Once we get down to actual coding, we have all this design work already done and a set of tests which will continually verify the new code we write. So while TDD is a design methodology it still ends up providing a nice set of tests. So let's get our class written.
Hide code highlighting
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | class My_Fleet implements Countable { protected $_ships = array(); public function addShip($shipName) { $this->_ships[] = $shipName; } public function count() { return count($this->_ships); } public function getShip($index) { return $this->_ships[ intval($index) ]; } } |
A working class, and our tests will all pass! Maybe later we'll change the tests to allow for Ship objects which implement __toString() to print the ship name. A small change to the tests to strval() the getShip() return value and our tests would be updated for this changed behaviour.
Original: An Introduction to the Art of Unit Testing in PHP
Rating:




<< Please, rate this articleRelated articles:
6 Tools To Be An Effective Web Developer
Understanding scope in object oriented JavaScript
Integrating FCKeditor with Zend_Form
Automated Testing Using Zend Framework
A caching pattern for models