$30 off During Our Annual Pro Sale. View Details »

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.

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

    View Slide

  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

    View Slide

  3. What
    He
    Said
    Friday, June 28, 13

    View Slide

  4. Legacy Code?
    Friday, June 28, 13

    View Slide

  5. Legacy Code IS
    difficult to change
    often unstructured
    and likely incomprehensible
    “Test resistant”
    Friday, June 28, 13

    View Slide

  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

    View Slide

  7. Should Vs. Must
    Friday, June 28, 13

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  11. Break Dependencies
    Without Tests In Place? Migraine.
    Friday, June 28, 13

    View Slide

  12. Mechanism For
    Feedback
    Friday, June 28, 13

    View Slide

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

    View Slide

  14. Friday, June 28, 13

    View Slide

  15. Friday, June 28, 13

    View Slide

  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

    View Slide

  17. //features/bootstrap/FeatureContext.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

    View Slide

  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

    View Slide

  19. The FizzBuzzer
    Because, really, people will fund anything
    Friday, June 28, 13

    View Slide

  20. Friday, June 28, 13

    View Slide

  21. Anonymous users can run
    FizzBuzz but only up to to the
    number 100
    Friday, June 28, 13

    View Slide

  22. Whether the job is big
    or small, do it right or
    not at all
    Friday, June 28, 13

    View Slide

  23. Identify Change Points
    Friday, June 28, 13

    View Slide

  24. 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);
    }
    ?>




    FizzBuzz!

    Result

    Friday, June 28, 13

    View Slide

  25. Write Pinning Tests
    Friday, June 28, 13

    View Slide

  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

    View Slide

  27. Friday, June 28, 13

    View Slide

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

    View Slide

  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

    View Slide

  30. Friday, June 28, 13

    View Slide

  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

    View Slide

  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

    View Slide

  33. Friday, June 28, 13

    View Slide

  34. Friday, June 28, 13

    View Slide

  35. Friday, June 28, 13

    View Slide

  36. summary
    Friday, June 28, 13

    View Slide

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

    View Slide

  38. Create Abstractions
    expose the API you want, guided by tests
    Friday, June 28, 13

    View Slide

  39. 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

    View Slide

  40. 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

    View Slide

  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

    View Slide

  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

    View Slide

  43. function fizzbuzz_for_range($start, $end) {
    $processor = new RangeProcessor();
    return $processor->process($start, $end);
    }
    Friday, June 28, 13

    View Slide

  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

    View Slide

  45. Thank You! Questions?
    http://speakerdeck.com/jcarouth
    @jcarouth | [email protected]
    https://joind.in/8683
    Friday, June 28, 13

    View Slide