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

Introducing Tests in Legacy PHP Applications - MidwestPHP 2014

Introducing Tests in Legacy PHP Applications - MidwestPHP 2014

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 yield better results.

Jeff Carouth

March 16, 2014
Tweet

More Decks by Jeff Carouth

Other Decks in Programming

Transcript

  1. also known as testing from the pit of despair
    Introducing Tests in
    Legacy PHP Applications
    PRESENTED BY
    JEFF CAROUTH
    @jcarouth

    View full-size slide

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

    View full-size slide

  3. Should Vs. Must
    dilemma

    View full-size slide

  4. RESIST
    the urge to
    Rewrite

    View full-size slide

  5. REFACTOR
    only what is necessary
    for tests

    View full-size slide

  6. Algorithms
    2.

    View full-size slide

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

    View full-size slide

  8. Break Dependencies
    Without Tests In Place? Migraine.

    View full-size slide

  9. Mechanism For
    Feedback

    View full-size slide

  10. Acceptance Tests
    masquerade as characterization tests

    View full-size slide

  11. //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"!

    View full-size slide

  12. //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!"'),!
    );!
    }!
    }

    View full-size slide

  13. Test Introduction Algorithm
    1. Identify what you need to change.

    2. Write characterization test for behavior.

    3. Locate test point for unit test.

    4. Break dependencies.

    5. Write tests.

    6. Make changes and refactor.

    View full-size slide

  14. The FizzBuzzer
    Because, really, people will fund anything

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  17. Identify Change Points

    View full-size slide

  18. 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!
    !

    View full-size slide

  19. Write Characterization Tests

    View full-size slide

  20. 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"!

    View full-size slide

  21. Refactor and Make Changes

    View full-size slide

  22. !
    Scenario:
    !
    !
    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"!

    View full-size slide

  23. 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']);!
    }!
    }!

    View full-size slide

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

    View full-size slide

  25. +3 feature tests
    +2 Unit tests
    Added

    View full-size slide

  26. Freedom to Refactor
    FEARLESSLY

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  29. 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)!
    );!
    }!
    }

    View full-size slide

  30. 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;!
    }!
    }!

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  33. Break Dependencies
    creating seams is your friend

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  37. 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;!
    }!
    }!

    View full-size slide

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

    View full-size slide

  39. ! phpunit tests/OrderTest.php!
    PHPUnit 3.7.28 by Sebastian Bergmann.!
    !
    Configuration read from /projects/fizzbuzzer/phpunit.xml.dist!
    !
    .!
    !
    Time: 422 ms, Memory: 4.00Mb!
    !
    OK (1 test, 1 assertion)

    View full-size slide

  40. Test Introduction Algorithm
    1. Identify what you need to change.

    2. Write characterization test for behavior.

    3. Locate test point for unit test.

    4. Break dependencies.

    5. Write tests.

    6. Make changes and refactor.

    View full-size slide

  41. Recommended Reading
    Working Effectively with Legacy Code!
    by Michael Feathers
    Modernize Your Legacy PHP Application!
    by Paul M. Jones!
    mlaphp.com

    View full-size slide

  42. Thanks.
    http://speakerdeck.com/jcarouth
    Q&A
    Thanks.Q&A
    https://joind.in/10563
    @jcarouth — #mwphp14

    View full-size slide