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.

0f930e13633535c1c4041e95b8881308?s=128

Jeff Carouth

March 16, 2014
Tweet

Transcript

  1. also known as testing from the pit of despair Introducing

    Tests in Legacy PHP Applications PRESENTED BY JEFF CAROUTH @jcarouth
  2. What He Said

  3. Legacy 1.

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

    incomprehensible “Test resistant”
  5. Should Vs. Must dilemma

  6. RESIST the urge to Rewrite

  7. REFACTOR only what is necessary for tests

  8. Algorithms 2.

  9. 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
  10. Break Dependencies Without Tests In Place? Migraine.

  11. Mechanism For Feedback

  12. Acceptance Tests masquerade as characterization tests

  13. None
  14. None
  15. //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"!
  16. //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!"'),! );! }! }
  17. 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.
  18. Example 3.

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

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

    the number 100
  22. Whether the job is big or small, do it right

    or not at all
  23. Identify Change Points

  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): ?>
  25. Write Characterization Tests

  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"!
  27. None
  28. Refactor and Make Changes

  29. ! 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"!
  30. None
  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']);! }! }!
  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,! );! }!
  33. None
  34. None
  35. None
  36. +3 feature tests +2 Unit tests Added

  37. Freedom to Refactor FEARLESSLY

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

  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);! }! }! ?>!
  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)! );! }! }
  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;! }! }!
  42. function fizzbuzz_for_range($start, $end) {! $f = array();! ! for ($i

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

    $end);! }!
  44. Break Dependencies creating seams is your friend

  45. namespace Fizzbuzzer;! ! class Order! {! public $id;! ! public

    function getPayments()! {! $paymentDao = new PaymentDao();! return $paymentDao->getOrderPayments($this->id);! }! }
  46. namespace tests\Fizzbuzzer;! ! use Fizzbuzzer\Order;! ! class OrderTest extends \PHPUnit_Framework_TestCase!

    {! /** @test */! public function shouldReturnPaymentsAppliedToOrder()! {! $o = new Order();! $this->assertNotEmpty($o->getPayments());! }! }!
  47. class Order! {! public $id;! ! public function getPayments()! {!

    $paymentDao = $this->getPaymentDao();! return $paymentDao->getOrderPayments($this->id);! }! ! protected function getPaymentDao()! {! return new PaymentDao();! }! }!
  48. 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;! }! }!
  49. 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());! }! }!
  50. ! 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)
  51. 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.
  52. Recommended Reading Working Effectively with Legacy Code! by Michael Feathers

    Modernize Your Legacy PHP Application! by Paul M. Jones! mlaphp.com
  53. Thanks. http://speakerdeck.com/jcarouth Q&A Thanks.Q&A https://joind.in/10563 @jcarouth — #mwphp14