This talk was designed for PHP developers with limited or no experience in unit testing. I focus on describing the problem of fear-driven-development, and how test-driven-development can be used to improve the quality of your code.
2. James Fuller
Web Developer
http://www.jblotus.com
Started unit testing in response to a new job
developing on a large, buggy legacy
application used by millions
3. Afraid to make even tiny code changes due to
unpredictable side-effects?
Been burned by bugs that came back to life?
Does sketchy code keep you up at night?
4. Imagine your code as a dark twisted forest that
you must navigate.
Nothing makes sense, there are no clear paths
and you are surrounded by bugs. Lots of bugs.
How would you handle being stuck in this
situation?
5. Some will be brave and foolish, cutting
corners to move quickly through the
forest.
Some will be paralyzed by the thought
of making the wrong move.
Most will succumb to the bugs or die a
horrible death trying to find a way out.
6. Modify code
Broke
something Run feature
God I hope Doesn't work
this works
Modify Code
7. Write tests
Use PHPUnit to automate your tests
Practice Test-Driven-Development
Improve your design
Enjoy the added value
8. Can you prove it works? automatically?
Can you change it and know it still works?
without loading the browser?
Can you promise that if it breaks, it will never
break like that again?
What about edge cases? You do care about
edge cases, right?
9. All developers practice some form of testing.
Most PHP developers do it in the browser.
What we want to do is automate testing
with PHPUnit.
Don't confuse apathy with laziness. Good
coders automate repetitive tasks because
they are lazy.
10. Before you write new code
When you fix a bug
Before you begin refactoring code
As you go, improving code coverage
We can use Test-Driven-Development to help
us maintain disclipline in writing our unit tests.
11. The principle of unit testing is to verify that a
unit of code will function correctly in
isolation.
A unit is the smallest testable part of an
application, usually a function or method.
12. Understand
Requirements
Refactor Write tests
Run Tests Run Tests
(PASS) (FAIL)
Write Code
13. Focus on design of system before
implementation
Changes are easier to make, with less side
effects
Documentation via example
Less FEAR, more CONFIDENCE
You have tests at the end!
14. PHPUnit is an automated testing framework
PHP development based upon xUnit (java).
PHPUnit gives us an easy way to write tests,
generate code coverage and improve the
quality of our code.
15. Command line test runner
Mature and expansive assertion library
Code coverage
Mock objects
Lots of extensions
Fast & Configurable
16. PEAR
best for system wide access to phpunit
hard to downgrade or change versions
dependencies can be tricky
Github
allows the most control
easy to pack up in your version control system
lots of steps
Phar (PHP Archive)
a single phar can be easily included in your projects
in development
many features don't work or don't work as expected
17. Upgrade PEAR to the latest version
sudo pear upgrade PEAR
Install PHPUnit
pear config-set auto_discover 1
pear install pear.phpunit.de/PHPUnit
This will install the latest release of PHPUnit.
Installing older versions is a bit trickier.
19. Checkout source from github
mkdir phpunit && cd phpunit
git clone git://github.com/sebastianbergmann/phpunit.git
git clone git://github.com/sebastianbergmann/dbunit.git
…
Still need PEAR to install YAML component
pear install pear.symfony-project.com/YAML
Make sure to add all these directories to your include_path
Checking out specific versions of packages:
cd phpunit
git checkout 3.6
20. Usage
phpunit [switches] UnitTest [UnitTest.php]
phpunit [switches] <directory>
Common Switches
--stop-on-error Stop upon first error.
--stop-on-failure Stop upon first error or failure.
--debug Output debug info (test name)
-v|--verbose Output verbose information.
--bootstrap <file> Load this file before tests
--configuration <file> Specify config file
(default phpunix.xml)
21. PHPUnit runtime options can be defined
using command line switches and/or xml
(phpunit.xml)
Bootstrap your application as needed before
tests run (bootstrap.php)
Define paths for code coverage output, logs,
etc.
22. It's easy!! Include the autoloader in your
bootstrap or in your test cases:
<?php
require_once 'PHPUnit/Autoload.php';
23. A test suite contains any number of test
cases.
A test case is a class that contains individual
tests.
A test is a method that contains the code to
test something.
26. A Fixture represents the known state of the
world.
Object instantiation
Database Records
Preconditions & Postconditions
PHPUnit offers several built-in methods to
reduce the clutter of setting up fixtures.
27. class FooTest extends PHPUnit_Framework_TestCase {
//runs before each test method
protected function setUp() {}
//runs after each test method
protected function tearDown() {}
//runs before any tests
public static function setUpBeforeClass() {}
//runs after all tests
public static function tearDownAfterClass() {}
}
28. class FooTest extends PHPUnit_Framework_TestCase {
protected $Foo;
protected function setUp() {
$this->Foo = new Foo;
}
public function testFoo () {
$this->assertNull($this->Foo->method());
}
public function testFoo1() {
$this->assertNull($this->Foo->otherMethod());
}
}
29. //method being tested in class Foo
public function reverseConcat($a, $b) {
return strrev($a . $b);
}
//test method in class FooTest
public function testReverseConcat() {
$sut = new Foo;
$actual = $sut->reverseConcat('Foo', 'Bar');
$expected = 'raBooF';
$this->assertEquals($expected, $actual);
}
30. An assertion is how we declare an expectation.
Well-tested systems rely upon verification, not
developer intuition.
Things we can assert with PHPUnit
return values
certain methods/objects were called via Test Doubles
echo/print output
32. When assertions evaluate to TRUE the test will emit a PASS
$this->assertTrue(true);
$this->assertFalse(false);
$this->assertEquals('foo', 'foo');
$this->assertEmpty(array());
$this->assertNotIsSame(new stdClass, new stdClass);
$this->assertArrayHasKey('foo', array('foo' => 'bar'));
$this->assertCount(2, array('apple', 'orange'));
33. When assertions evaluate to FALSE the test will emit a FAILURE
$this->assertTrue(false);
$this->assertFalse(true);
$this->assertEquals('foo', 'bar');
$this->assertEmpty(array('foo'));
$this->assertIsSame(new stdClass, new stdClass);
$this->assertArrayHasKey('foo', array());
$this->assertCount(2, array('apple'));
34. game has 2 players
players pick either rock, paper, or scissors
rock beats scissors
paper beats rock
scissors beats paper
round is a draw if both players pick the same
object
35. Isolation means that a unit of code is not
affected by external factors.
In practice we can't always avoid external
dependencies, but we benefit from trying to
reduce their impact on our tests.
This means we need to be able to substitute
external dependencies with test doubles.
36. Test doubles are objects we use to substitute
dependencies in our system under test.
Stubs return prebaked values when it's
methods are called.
Mocks are like programmable stubs. You can
verify that methods are called correctly and
perform more complex verification.
37. PHPUnit provides a built in mocking
framework.
You can easily create test doubles that you
can inject into your system under test.
This allows you to swap out complex
dependencies with controllable and
verifiable objects.
38. public function testStub() {
$stub = $this->getMock('Stub', array('foo'));
$stub->expects($this->once())
->method('foo')
->will($this->returnValue('bar'));
$actual = $stub->foo();
$this->assertEquals('bar', $actual, "should
have returned 'bar'");
}
39. Two ways to generate mock objects
getMock() with arguments
Mock Builder API
43. $mock = $this->getMockBuilder('Foo')
->setMethods(array('someMethod'))
->getMock();
$mock->expects($this->any())
->method('someMethod')
->will($this->throwException(
new RuntimeException
));
//throws a RuntimeException
$mock->someMethod();
44. class WebService {
protected $Http;
public function __construct(Http $http) {
$this->Http = $http;
}
public function getUrl($url = '') {
return $this->Http->get($url);
}
}
45. require_once 'PHPUnit/Autoload.php';
require_once 'WebService.php';
class TestWebService extends PHPUnit_Framework_TestCase {
public function testGetUrl_callsHttpCorrectly() {
$http = $this->getMockBuilder('FakeClassName')
->setMethods(array('get'))
->setMockClassName('Http')
->getMock();
$http->expects($this->once())
->method('get')
->with($url);
//inject the mock via constructor
$sut = new WebService($http);
//test Http get
$sut->getUrl('http://google.com');
}
}
47. class Dependency {
}
class SystemUnderTest {
public function badIdea() {
$dependency = new Dependency;
}
}
48. class TwitterClient {
function getTweets() {
$url = 'api.twitter.com/v2/tweets/...';
return Http::consume($url);
}
}
49. class TwitterClient {
protected $Http;
public function __construct(Http $http) {
$this->Http = $http;
}
public function getTweets() {
$url = 'api.twitter.com/v2/tweets/...';
return $this->Http->consume($url);
}
}
50. class TestTwitterClient extends
PHPUnit_Framework_TestCase {
public function testCantMockTwitter() {
$mock = $this->getMockBuilder(Http')
->setMethods(array('consume'))
->getMock();
$sut = new TwitterClient($mock);
$sut->getTweets();
}
}
51. class Singleton {
public $data;
protected static $instance;
private function __construct() {}
private function __clone() {}
public function getInstance() {
if (!self::$instance) {
self::$instance = new self;
}
return self::$instance;
}
}
52. class SystemUnderTest {
public function setData($data) {
Singleton::getInstance()->data = $data;
}
public function getData() {
return Singleton::getInstance()->data;
}
}
53. class TestSystemUnderTest extends
PHPUnit_Framework_TestCase {
public function testSingletonsAreFineAtFirst() {
$sut = new SystemUnderTest;
$sut->setData('foo');
//passes
$this->assertEquals('foo', $sut->getData());
}
public function testSingletonsNeverDie() {
$sut = new SystemUnderTest;
$actual = $sut->getData();
//fails
$this->assertNull($actual);
}
}
54. Many frameworks come with built in testing
facilities, or the ability to easily integrate with
PHPUnit.
Try to use the built-in testing tools. They
usually do the heavy lifting of bootstrapping
your application.
55. Get your test harness working
Get your feet wet first, testing smaller/trivial
code
Manual verification is prudent
Improve your design, refactor
Write tests for new code
Write tests opportunistically