Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Introducing Tests in Legacy PHP Applications

Introducing Tests in Legacy PHP Applications

You know testing is beneficial to your project. You are familiar with merits and caveats of test-driven development. But the project you’re hacking on right now is what most would call a legacy application. How do you apply your test knowledge to an application that doesn’t lend itself to traditional unit testing? The answer most will give is, “you don’t,” but we’re going to look at ways to write tests now that will allow you to improve and refactor your application to evolve the code to a more manageable state. The traditional “easy” route to dealing with legacy code is to rewrite but a little patience and skill applied to writing tests will yeild better results.

0f930e13633535c1c4041e95b8881308?s=128

Jeff Carouth

June 28, 2013
Tweet

More Decks by Jeff Carouth

Other Decks in Programming

Transcript

  1. @jcarouth / #lsp13 also known as testing from the pit

    of despair Introducing Tests in Legacy PHP Applications Friday, June 28, 13
  2. Howdy! I am Jeff Carouth I have written many, many

    lines of legacy code. @jcarouth I work at Liftopia employing the techniques I am going to present today. Friday, June 28, 13
  3. What He Said Friday, June 28, 13

  4. Legacy Code? Friday, June 28, 13

  5. Legacy Code IS difficult to change often unstructured and likely

    incomprehensible “Test resistant” Friday, June 28, 13
  6. $body->compile_dir = $body->template_dir."/compiled"; //determine the controller that should be used

    for this request based on which // controller name is passed through the query string. If there is not a // controller name in the query string, we will assume that the request is for // the index page if ( true == isset( $_GET['controller'] ) && true == is_string( $_GET['controller']) && $_GET['c $page = trim( $_GET['controller'] ); unset( $_GET['controller'] ); } else { $page = ""; } // If we have a separate intro page defined, it should be rendered. if( SEPARATE_INTRO !== false && $page === "" ) { if( true == is_file( "controllers/" . SEPARATE_INTRO . ".php" ) ) { include_once "controllers/" . SEPARATE_INTRO . ".php"; } $body->display( SEPARATE_INTRO . ".html" ); } else { $content = new Smarty(); $content->template_dir = "html"; $content->compile_dir = $body->template_dir."/compiled"; if( $page === "" ) { $page = DEFAULT_PAGE; } // Determine what, if any, view should be presented to the client. If there is // not a view request assign an empty string (default view). Friday, June 28, 13
  7. Should Vs. Must Friday, June 28, 13

  8. RESIST the urge to Rewrite Friday, June 28, 13

  9. REFACTOR only what is necessary for tests Friday, June 28,

    13
  10. 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 Friday, June 28, 13
  11. Break Dependencies Without Tests In Place? Migraine. Friday, June 28,

    13
  12. Mechanism For Feedback Friday, June 28, 13

  13. Acceptance Tests masquerade as pinning tests Friday, June 28, 13

  14. Friday, June 28, 13

  15. Friday, June 28, 13

  16. //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" Friday, June 28, 13
  17. //features/bootstrap/FeatureContext.php <?php // A bunch of use statements and requires

    go here /** * Features context. */ class FeatureContext extends MinkContext { /** * Runs the fizzbuzz algorithm for given range. * * @When /^I run fizzbuzz on range "([^"]*)" to "([^"]*)"$/ */ public function iRunFizzBuzzOnRange($start, $end) { return array( new Step\When('I fill in "start" with "'.$start.'"'), new Step\When('I fill in "end" with "'.$end.'"'), new Step\When('I press "FizzBuzz!"'), ); } } Friday, June 28, 13
  18. 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. Friday, June 28, 13
  19. The FizzBuzzer Because, really, people will fund anything Friday, June

    28, 13
  20. Friday, June 28, 13

  21. Anonymous users can run FizzBuzz but only up to to

    the number 100 Friday, June 28, 13
  22. Whether the job is big or small, do it right

    or not at all Friday, June 28, 13
  23. Identify Change Points Friday, June 28, 13

  24. <?php require_once dirname(__FILE__) . "/global.php"; if (sizeof($_POST) <= 0) {

    header("Location: index.php"); } $start = filter_var($_POST['start'], FILTER_VALIDATE_INT); $end = filter_var($_POST['end'], FILTER_VALIDATE_INT); if (false === $start || false === $end) { die('Hackers go away!'); } require_once "include/fizzbuzz.funcs.php"; $results = fizzbuzz_for_range($start, $end); //log this run if the user is logged in if (user_logged_in()) { user_log_run(user_get_id(), $start, $end); } ?> <?php include dirname(__FILE__) . "/templates/header.phtml"; ?> <form class="form-inline" action="process.php" method="POST"> <input type="text" name="start" placeholder="Start" /> <input type="text" name="end" placeholder="End" /> <button type="submit" class="btn btn-primary">FizzBuzz!</button> </form> <h2>Result</h2> <?php if (false !== $results): ?> Friday, June 28, 13
  25. Write Pinning Tests Friday, June 28, 13

  26. 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" Friday, June 28, 13
  27. Friday, June 28, 13

  28. Refactor and Make Changes Friday, June 28, 13

  29. 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" Friday, June 28, 13
  30. Friday, June 28, 13

  31. 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']); } } Friday, June 28, 13
  32. 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, ); } Friday, June 28, 13
  33. Friday, June 28, 13

  34. Friday, June 28, 13

  35. Friday, June 28, 13

  36. summary Friday, June 28, 13

  37. Freedom to Refactor FEARLESSLY Friday, June 28, 13

  38. Create Abstractions expose the API you want, guided by tests

    Friday, June 28, 13
  39. <?php require_once dirname(__FILE__) . "/global.php"; //...snip... require_once dirname(__FILE__) . "/include/fizzbuzz.funcs.php";

    $to_process = fizzbuzz_normalize_range( $start, $end, user_logged_in()); $results = false; if (false !== $to_process['can_process']) { $results = fizzbuzz_for_range( $to_process['start'], $to_process['end'] ); //log this run if the user is logged in if (user_logged_in()) { user_log_run(user_get_id(), $start, $end); } } ?> Friday, June 28, 13
  40. <?php namespace Tests\FizzBuzzer; class RangeProcessorTest extends \PHPUnit_Framework_TestCase { /** @var

    \FizzBuzzer\RangeProcessor */ protected $rangeProcessor; public function setUp() { $this->rangeProcessor = new \FizzBuzzer\RangeProcessor(); } /** * @test */ public function shouldReturnValuesForSimpleRange() { $this->assertEquals( array(1, 2, 'Fizz', 4, 'Buzz'), $this->rangeProcessor->process(1, 5) ); } } Friday, June 28, 13
  41. 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; } } Friday, June 28, 13
  42. function fizzbuzz_for_range($start, $end) { $f = array(); for ($i =

    $start; $i <= $end; $i++) { $f[] = fizzbuzz_of($i); } return $f; } Friday, June 28, 13
  43. function fizzbuzz_for_range($start, $end) { $processor = new RangeProcessor(); return $processor->process($start,

    $end); } Friday, June 28, 13
  44. 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. Friday, June 28, 13
  45. Thank You! Questions? http://speakerdeck.com/jcarouth @jcarouth | jcarouth@gmail.com https://joind.in/8683 Friday, June

    28, 13