fighting fear-driven-development with phpunit

56
South Florida PHP Meetup Group

Upload: james-fuller

Post on 10-May-2015

3.912 views

Category:

Technology


3 download

DESCRIPTION

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.

TRANSCRIPT

Page 1: Fighting Fear-Driven-Development With PHPUnit

South Florida PHP Meetup Group

Page 2: Fighting Fear-Driven-Development With PHPUnit

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

Page 3: Fighting Fear-Driven-Development With PHPUnit

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?

Page 4: Fighting Fear-Driven-Development With PHPUnit

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?

Page 5: Fighting Fear-Driven-Development With PHPUnit

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.

Page 6: Fighting Fear-Driven-Development With PHPUnit

Modify code

Run feature

Doesn't work

Modify Code

God I hope this works

Broke something

Page 7: Fighting Fear-Driven-Development With PHPUnit

Write tests Use PHPUnit to automate your tests Practice Test-Driven-Development Improve your design Enjoy the added value

Page 8: Fighting Fear-Driven-Development With PHPUnit

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?

Page 9: Fighting Fear-Driven-Development With PHPUnit

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.

Page 10: Fighting Fear-Driven-Development With PHPUnit

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.

Page 11: Fighting Fear-Driven-Development With PHPUnit

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.

Page 12: Fighting Fear-Driven-Development With PHPUnit

Understand Requirements

Write tests

Run Tests (FAIL)

Write Code

Run Tests (PASS)

Refactor

Page 13: Fighting Fear-Driven-Development With PHPUnit

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!

Page 14: Fighting Fear-Driven-Development With PHPUnit

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.

Page 15: Fighting Fear-Driven-Development With PHPUnit

Command line test runner

Mature and expansive assertion library

Code coverage

Mock objects

Lots of extensions

Fast & Configurable

Page 16: Fighting Fear-Driven-Development With PHPUnit

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

Page 17: Fighting Fear-Driven-Development With PHPUnit

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.

Page 18: Fighting Fear-Driven-Development With PHPUnit

pear install phpunit/DbUnit pear install phpunit/PHPUnit_Selenium pear install phpunit/PHPUnit_Story pear install phpunit/PHPUnit_TestListener_DBUS pear install phpunit/PHPUnit_TestListener_DBUS pear install phpunit/PHPUnit_TestListener_XHProf pear install phpunit/PHPUnit_TicketListener_Fogbugz pear install phpunit/PHPUnit_TicketListener_GitHub pear install phpunit/PHPUnit_TicketListener_GoogleCode pear install phpunit/PHPUnit_TicketListener_Trac pear install phpunit/PHP_Invoker

Page 19: Fighting Fear-Driven-Development With PHPUnit

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

Page 20: Fighting Fear-Driven-Development With PHPUnit

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)

Page 21: Fighting Fear-Driven-Development With PHPUnit

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.

Page 22: Fighting Fear-Driven-Development With PHPUnit

It's easy!! Include the autoloader in your bootstrap or in your test cases: <?php require_once 'PHPUnit/Autoload.php';

Page 23: Fighting Fear-Driven-Development With PHPUnit

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.

Page 24: Fighting Fear-Driven-Development With PHPUnit

/

lib/

Foo.php

tests/

suite/

lib/

FooTest.php

Page 25: Fighting Fear-Driven-Development With PHPUnit

<phpunit> <testsuites> <testsuite name="All Tests"> <directory>tests/suite</directory> </testsuite> <testsuite name="Some Tests"> <file>tests/suite/FooTest.php</file> <file>tests/suite/BarTest.php</file> </testsuite> </testsuites> </phpunit>

Page 26: Fighting Fear-Driven-Development With PHPUnit

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.

Page 27: Fighting Fear-Driven-Development With PHPUnit

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() {}

}

Page 28: Fighting Fear-Driven-Development With PHPUnit

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()); } }

Page 29: Fighting Fear-Driven-Development With PHPUnit

//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); }

Page 30: Fighting Fear-Driven-Development With PHPUnit

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

Page 31: Fighting Fear-Driven-Development With PHPUnit

assertArrayHasKey() assertClassHasAttribute() assertClassHasStaticAttribute() assertContains() assertContainsOnly() assertCount() assertEmpty() assertEqualXMLStructure() assertEquals() assertFalse() assertFileEquals() assertFileExists() assertGreaterThan() assertGreaterThanOrEqual() assertInstanceOf() assertInternalType() assertLessThan() assertLessThanOrEqual()

assertNull() assertObjectHasAttribute() assertRegExp() assertStringMatchesFormat() assertStringMatchesFormatFile() assertSame() assertSelectCount() assertSelectEquals() assertSelectRegExp() assertStringEndsWith() assertStringEqualsFile() assertStringStartsWith() assertTag() assertThat() assertTrue() assertXmlFileEqualsXmlFile() assertXmlStringEqualsXmlFile() assertXmlStringEqualsXmlString()

Page 32: Fighting Fear-Driven-Development With PHPUnit

$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'));

When assertions evaluate to TRUE the test will emit a PASS

Page 33: Fighting Fear-Driven-Development With PHPUnit

$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'));

When assertions evaluate to FALSE the test will emit a FAILURE

Page 34: Fighting Fear-Driven-Development With PHPUnit

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

Page 35: Fighting Fear-Driven-Development With PHPUnit

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.

Page 36: Fighting Fear-Driven-Development With PHPUnit

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.

Page 37: Fighting Fear-Driven-Development With PHPUnit

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.

Page 38: Fighting Fear-Driven-Development With PHPUnit

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'"); }

Page 39: Fighting Fear-Driven-Development With PHPUnit

Two ways to generate mock objects getMock() with arguments Mock Builder API

Page 40: Fighting Fear-Driven-Development With PHPUnit

$this->getMock( $class_name, $mocked_methods, $constructor_args, $mock_class_name, $call_original_constructor, $call_original_clone_constructor, $disable_autoload );

Page 41: Fighting Fear-Driven-Development With PHPUnit

$this->getMockBuilder($class_name) ->setMethods(array $methods) ->disableOriginalConstructor() ->disableOriginalClone() ->setConstructorArgs(array $args) ->setMockClassName($class_name) ->getMock();

Page 42: Fighting Fear-Driven-Development With PHPUnit

$mock = $this->getMockBuilder('Foo') ->setMethods(array('someMethod')) ->getMock(); $mock->expects($this->any()) ->method('someMethod') ->will( $this->onConsecutiveCalls('bar', 'baz') ); $mock->someMethod(); //returns 'bar' $mock->someMethod(); //returns 'baz'

Page 43: Fighting Fear-Driven-Development With PHPUnit

$mock = $this->getMockBuilder('Foo') ->setMethods(array('someMethod')) ->getMock(); $mock->expects($this->any()) ->method('someMethod') ->will($this->throwException( new RuntimeException )); //throws a RuntimeException $mock->someMethod();

Page 44: Fighting Fear-Driven-Development With PHPUnit

class WebService { protected $Http; public function __construct(Http $http) { $this->Http = $http; } public function getUrl($url = '') { return $this->Http->get($url); } }

Page 45: Fighting Fear-Driven-Development With PHPUnit

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'); } }

Page 46: Fighting Fear-Driven-Development With PHPUnit

Static method calls

Instantiating objects directly (hard coupling)

External resources (network, file system)

Globals ($_GLOBALS, Singletons)

Page 47: Fighting Fear-Driven-Development With PHPUnit

class Dependency { } class SystemUnderTest { public function badIdea() { $dependency = new Dependency; } }

Page 48: Fighting Fear-Driven-Development With PHPUnit

class TwitterClient { function getTweets() { $url = 'api.twitter.com/v2/tweets/...'; return Http::consume($url); } }

Page 49: Fighting Fear-Driven-Development With PHPUnit

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); } }

Page 50: Fighting Fear-Driven-Development With PHPUnit

class TestTwitterClient extends PHPUnit_Framework_TestCase { public function testCantMockTwitter() { $mock = $this->getMockBuilder(Http') ->setMethods(array('consume')) ->getMock(); $sut = new TwitterClient($mock); $sut->getTweets(); } }

Page 51: Fighting Fear-Driven-Development With PHPUnit

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; } }

Page 52: Fighting Fear-Driven-Development With PHPUnit

class SystemUnderTest { public function setData($data) { Singleton::getInstance()->data = $data; } public function getData() { return Singleton::getInstance()->data; } }

Page 53: Fighting Fear-Driven-Development With PHPUnit

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); } }

Page 54: Fighting Fear-Driven-Development With PHPUnit

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.

Page 55: Fighting Fear-Driven-Development With PHPUnit

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

Page 56: Fighting Fear-Driven-Development With PHPUnit