Slide 1

Slide 1 text

@jcarouth / #zendcon also known as testing from the pit of despair Introducing Tests in Legacy PHP Applications

Slide 2

Slide 2 text

JEFF CAROUTH DEVELOPER AT LIFTOPIA @jcarouth [email protected] Freenode: #phpmentoring

Slide 3

Slide 3 text

What He Said

Slide 4

Slide 4 text

Legacy 1.

Slide 5

Slide 5 text

Legacy Code IS difficult to change often unstructured and likely incomprehensible “Test resistant”

Slide 6

Slide 6 text

Should Vs. Must dilemma

Slide 7

Slide 7 text

RESIST the urge to Rewrite

Slide 8

Slide 8 text

REFACTOR only what is necessary for tests

Slide 9

Slide 9 text

Algorithms 2.

Slide 10

Slide 10 text

Legacy Code Change Algorithm 1. Identify change points. 2. Find test points. 3. Break dependencies. 4. Write tests. 5. Make changes and refactor. Source: Working Effectively with Legacy Code by Michael Feathers

Slide 11

Slide 11 text

Break Dependencies Without Tests In Place? Migraine.

Slide 12

Slide 12 text

Mechanism For Feedback

Slide 13

Slide 13 text

Acceptance Tests masquerade as pinning tests

Slide 14

Slide 14 text

No content

Slide 15

Slide 15 text

No content

Slide 16

Slide 16 text

//features/registereduser.feature Feature: Registered users can run fizzbuzz As a registered fizzbuzzer I want to be able to run fizzbuzz Background: Given An account exists for user "behat" password "letmein" When I am on homepage And I log in as "behat" with password "letmein" Scenario: Run fizzbuzz past anonymous limit When I run fizzbuzz on range "95" to "102" Then I should see "Buzz Fizz 97 98 Fizz Buzz 101 Fizz" Scenario: Cannot exceed maximum range When I run fizzbuzz on range "1" to "302" Then I should see "The range you requested is beyond the maximum… And I should see "298 299 FizzBuzz 301" But I should not see "299 FizzBuzz 301 302"

Slide 17

Slide 17 text

//features/bootstrap/FeatureContext.php

Slide 18

Slide 18 text

Test Introduction Algorithm 1. Identify what you need to change. 2. Write pinning test for impacted behavior. 3. Locate test point for unit test. 4. Break dependencies. 5. Write tests. 6. Make changes and refactor.

Slide 19

Slide 19 text

Example 3.

Slide 20

Slide 20 text

The FizzBuzzer Because, really, people will fund anything

Slide 21

Slide 21 text

No content

Slide 22

Slide 22 text

Anonymous users can run FizzBuzz but only up to to the number 100

Slide 23

Slide 23 text

Whether the job is big or small, do it right or not at all

Slide 24

Slide 24 text

Identify Change Points

Slide 25

Slide 25 text

FizzBuzz!

Result

Slide 26

Slide 26 text

Write Pinning Tests

Slide 27

Slide 27 text

Feature: Anonymous Run FizzBuzz As a non-registered fizzbuzz enthusiast I need to be able to run fizzbuzz on a range of numbers So that I can get my fix of fizzbuzziness Scenario: Anonymous users should be able to run the fizzbuzzer When I am on homepage And I run fizzbuzz on range "12" to "20" Then I should see "Fizz 13 14 FizzBuzz 16 17 Fizz 19 Buzz" Scenario: Anonymous users should be able to run a second fizzbuzz When I am on homepage And I run fizzbuzz on range "3" to "5" Then I should see "Fizz 4 Buzz" When I run fizzbuzz on range "9" to "12" Then I should see "Fizz Buzz 11 Fizz"

Slide 28

Slide 28 text

No content

Slide 29

Slide 29 text

Refactor and Make Changes

Slide 30

Slide 30 text

Then I should see "Fizz 13 14 FizzBuzz 16 17 Fizz 19 Buzz" Scenario: Anonymous users should be able to run a second fizzbuzz When I am on homepage And I run fizzbuzz on range "3" to "5" Then I should see "Fizz 4 Buzz" When I run fizzbuzz on range "9" to "12" Then I should see "Fizz Buzz 11 Fizz" Scenario: Anonymous users should not be able to exceed 100 When I am on homepage And I run fizzbuzz on range "95" to "101" Then I should see "101 is beyond the maximum value, 100" And I should see "Buzz Fizz 97 98 Fizz Buzz"

Slide 31

Slide 31 text

No content

Slide 32

Slide 32 text

class FizzBuzzTest extends \PHPUnit_Framework_TestCase { /** * @test */ public function normalizeRangeForAnonShouldTruncateAt100() { $normalizedRange = fizzbuzz_normalize_range(95, 101, false); $this->assertTrue($normalizedRange['can_process']); $this->assertEquals(100, $normalizedRange['end']); } /** * @test */ public function signedInUsersCanUseIntegersOver100() { $range = fizzbuzz_normalize_range(95, 101, true); $this->assertTrue($range['can_process']); $this->assertEquals(101, $range['end']); } }

Slide 33

Slide 33 text

function fizzbuzz_normalize_range($start, $end, $is_user = false) { $can_process = true; $message = ''; if (!$is_user) { //anon can only fizzbuzz for ranges up to 100 if ($end > 100) { $message = '$end.'is 'beyond the maximum value, 100” $end = 100; } } return array( 'can_process' => $can_process, 'message' => $message, 'start' => $start, 'end' => $end, ); }

Slide 34

Slide 34 text

No content

Slide 35

Slide 35 text

No content

Slide 36

Slide 36 text

No content

Slide 37

Slide 37 text

+3 feature tests +2 Unit tests Added

Slide 38

Slide 38 text

Freedom to Refactor FEARLESSLY

Slide 39

Slide 39 text

Create Abstractions expose the API you want, guided by tests

Slide 40

Slide 40 text

Slide 41

Slide 41 text

rangeProcessor = new \FizzBuzzer\RangeProcessor(); } /** * @test */ public function shouldReturnValuesForSimpleRange() { $this->assertEquals( array(1, 2, 'Fizz', 4, 'Buzz'), $this->rangeProcessor->process(1, 5) ); } }

Slide 42

Slide 42 text

class RangeProcessor { public function process($start, $end) { $results = array(); foreach (range($start, $end) as $number) { $results[] = $this->fizzbuzzFor($number); } return $results; } private function fizzbuzzFor($number) { if ($number % 15 === 0) { return 'FizzBuzz'; } elseif ($number % 3 === 0) { return 'Fizz'; } elseif ($number % 5 === 0) { return 'Buzz'; } return $number; } }

Slide 43

Slide 43 text

function fizzbuzz_for_range($start, $end) { $f = array(); for ($i = $start; $i <= $end; $i++) { $f[] = fizzbuzz_of($i); } return $f; }

Slide 44

Slide 44 text

function fizzbuzz_for_range($start, $end) { $processor = new RangeProcessor(); return $processor->process($start, $end); }

Slide 45

Slide 45 text

Break Dependencies creating seams is your friend

Slide 46

Slide 46 text

namespace Fizzbuzzer; class Order { public $id; public function getPayments() { $paymentDao = new PaymentDao(); return $paymentDao->getOrderPayments($this->id); } }

Slide 47

Slide 47 text

namespace tests\Fizzbuzzer; use Fizzbuzzer\Order; class OrderTest extends \PHPUnit_Framework_TestCase { /** @test */ public function shouldReturnPaymentsAppliedToOrder() { $o = new Order(); $this->assertNotEmpty($o->getPayments()); } }

Slide 48

Slide 48 text

class Order { public $id; public function getPayments() { $paymentDao = $this->getPaymentDao(); return $paymentDao->getOrderPayments($this->id); } protected function getPaymentDao() { return new PaymentDao(); } }

Slide 49

Slide 49 text

class Order { public $id; public function getPayments() { $paymentDao = $this->getPaymentDao(); return $paymentDao->getOrderPayments($this->id); } public function setPaymentDao(PaymentDao $paymentDao) { $this->paymentDao = $paymentDao; } protected function getPaymentDao() { if (null === $this->paymentDao) { $this->paymentDao = new PaymentDao(); } return $this->paymentDao; } }

Slide 50

Slide 50 text

class OrderTest extends \PHPUnit_Framework_TestCase { /** @test */ public function shouldReturnPaymentsAppliedToOrder() { $paymentDao = $this->getMock('\\Fizzbuzzer\\PaymentDao'); $paymentDao->expects($this->any()) ->method('getOrderPayments') ->will($this->returnValue(array( $this->getMock('\\Fizzubzzer\\Payment') ))); $o = new Order(); $o->setPaymentDao($paymentDao); $this->assertNotEmpty($o->getPayments()); } }

Slide 51

Slide 51 text

Test Introduction Algorithm 1. Identify what you need to change. 2. Write pinning test for impacted behavior. 3. Locate test point for unit test. 4. Break dependencies. 5. Write tests. 6. Make changes and refactor.

Slide 52

Slide 52 text

Thanks. http://speakerdeck.com/jcarouth Q&A http://joind.in/9059 Jeff Carouth // @jcarouth // #zendcon13