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

Testing the Untestable Zendcon 2015

973e0861866767b3940a6dbb5aac3642?s=47 Matt Land
October 21, 2015

Testing the Untestable Zendcon 2015

973e0861866767b3940a6dbb5aac3642?s=128

Matt Land

October 21, 2015
Tweet

More Decks by Matt Land

Other Decks in Programming

Transcript

  1. Testing the Untestable ZendCon 2015

  2. Matt Land Code Samples: github.com/matt-land Slides: speakerdeck.com/mattland/ linkedin.com/in/matthewland CERTIFIED A

    R C H I T E CT 2 https://joind.in/15548
  3. Testing the Untestable • Getting Started • Reflection & Static

    • Database Coupling • Network Services • Do I Need CI • Code Coverage • Ice Cream Cones and BDD • Intermittent Failure To Launch • My Worst UI Test • Sticky Bits • Its Not Me, Its the Environment
  4. Getting Started: Just Do It Noise: • PHPUnit • Asserts

    • Composer/Autoloaders • Namespaces • Coding Standards • Continuous Integration • Best Practices • Mocks Not Noise: • Writing Tests • Passing Tests
  5. OO Classes

  6. OO Classes

  7. PROTECTED PRIVATE PRIVATE PRIVATE PRIVATE PROTECTED PRIVATE PRIVATE PROTECTED PRIVATE

    OO Classes
  8. OO Classes

  9. None
  10. • Sample application: • https://github.com/matt-land/test-samples

  11. Continues { "category": "Young Adult", "records": 5, "meta": { "next":

    "\/whatever?q=345w5", "prev": "\/whatever?q=92342" }, "books": [ { "title": "The Carnival at Bray", "author": "Jessie Ann Foley", "publisher": "Elephant Rock", "price": "12.95", "isbn": "97809895115597" }, { "title": "The Crossover", "author": "Kwame Alexander", "publisher": "Houghton", "price": "16.99", "isbn": "9780544107717" }, { "title": "The Gospel of Winter", "author": "Brendan Kiely", "publisher": "Simon & Schuster\/Margaret K. McElderry", "price": "17.99", "isbn": "9781442484894" }, { "title": "I\u2019ll Give You the Sun", "author": "Jandy Nelson", "publisher": "Dial", "price": "17.99", "isbn": "9780803734968" }, { "title": "Jackaby", "author": "William Ritter", "publisher": "William Ritter", "price": "16.95", "isbn": "9781616203535" } ] } Sample Application
  12. public function __construct($jsonString = '[]') { $this->pageDataForView = json_decode($jsonString); $this->books

    = isset($this->pageDataForView->books) ? $this->pageDataForView- >books : null ; } /** * The first method I want to test */ private function sortBooksByAuthor() { //usort is going to modify $books by reference, //so lets copy it to prevent side effects $sortedBooks = $this->books; usort($sortedBooks, function($a, $b) { /** * sort by author last name, so first get the last name * or last segment of the name. Will not work with JR/III suffixes */ $authorA = explode(" ", trim($a->author)); $authorB = explode(" ", trim($b->author)); return strcmp($authorA[count($authorA)-1], $authorB[count($authorB)-1]); }); return $sortedBooks; } /** * @param $books * @return mixed * The second method I want to test Private Method I want to test
  13. Test My Method #1 • Push a dataset into my

    database • Write a selenium test • Navigate the Dom tree • Validate results • Test a lot of other things • Slow down the test suite for one test
  14. Test My Method #2 • Push a dataset into my

    database • Add code die(print_r($result,1)) in my app • Look at the code • Comment out the code • Never test it again • Hope no one ever breaks the working function • Hope I never accidentally check in debug code
  15. Test My Method #3 • Make fake data and results

    • Have PHP make a copy of the class • Have PHP make a copy of the function • Change the copied function to public • Call the function with my data • Verify results
  16. OO Classes

  17. <?php namespace SamplesTest; use Samples\Render; use ReflectionClass; use ReflectionMethod; use

    PHPUnit_Framework_TestCase; /** * Created by IntelliJ IDEA. * User: mland * Date: 4/17/15 * Time: 11:25 AM */ date_default_timezone_set('America/Chicago'); class RenderTest extends PHPUnit_Framework_Testcase { protected $jsonBody; /** * this is to simulate the json data we feed to the template */ public function setUp() { $this->jsonBody = json_decode( file_get_contents(__DIR__ . '/pageDataForView.json') ); } public function _testRenderer() { Setup $this->jsonBody for all tests
  18. } public function _testRenderer() { $renderObj = new Render(json_encode($this->jsonBody)); $rendering

    = $renderObj->toHtml(); $this->fail('This is complex: ' . PHP_EOL . $rendering . " Lets test the methods instead."); } /** * reflecting a method, where we have to set the value in the class first */ public function testSortBooksByAuthor() { $reflectedClass = new ReflectionClass('\\Samples\\Render'); $realClass = $reflectedClass->newInstance(); $booksProperty = $reflectedClass->getProperty('books'); $booksProperty->setAccessible(1); $booksProperty->setValue($realClass, $this->jsonBody->books); $sortByAuthorMethod = $reflectedClass->getMethod('sortBooksByAuthor'); $sortByAuthorMethod->setAccessible(true); $sortedBooks = $sortByAuthorMethod->invoke($realClass); $this->assertEquals('Kwame Alexander', $sortedBooks[0]->author); $this->assertEquals('William Ritter', $sortedBooks[4]->author); } /** * this time written statically/no object oriented dependency * Much easier */ Test that uses reflection 10 LOC
  19. private function sortBooksByAuthor() { //usort is going to modify $books

    by reference, //so lets copy it to prevent side effects $sortedBooks = $this->books; usort($sortedBooks, function ($a, $b) { /** * sort by author last name, so first get the last name * or last segment of the name. Will not work with JR/III suffixes */ $authorA = explode(" ", trim($a->author)); $authorB = explode(" ", trim($b->author)); return strcmp($authorA[count($authorA) - 1], $authorB[count($authorB) - 1]); }); return $sortedBooks; } public function testSortBooksByAuthor() { $reflectedClass = new ReflectionClass('\\Samples\\Render'); $realClass = $reflectedClass->newInstance(); $booksProperty = $reflectedClass->getProperty('books'); $booksProperty->setAccessible(1); $booksProperty->setValue($realClass, $this->jsonBody->books); $sortByAuthorMethod = $reflectedClass->getMethod('sortBooksByAuthor'); $sortByAuthorMethod->setAccessible(true); $sortedBooks = $sortByAuthorMethod->invoke($realClass); $this->assertEquals('Kwame Alexander', $sortedBooks[0]->author); $this->assertEquals('William Ritter', $sortedBooks[4]->author); }
  20. None
  21. /** * @param $books * @return mixed * The second

    method I want to test */ private static function sortBooksByPrice($books) { //usort is going to modify $books by reference, //so lets copy it to prevent side effects $sortedBooks = $books; usort($sortedBooks, function($a, $b) { /** * compare decimal price */ return $a->price >= $b->price; }); return $sortedBooks; } public function testSortBooksByPrice() { $realClass = new Render(); Second Method I want to test Changed to static Parameter is passed in Gave up $this, but still have self::
  22. */ return $a->price >= $b->price; }); return $sortedBooks; } public

    function testSortBooksByPrice() { $realClass = new Render(); $sortByPriceMethod = new ReflectionMethod( $realClass, 'sortBooksByPrice' ); $sortByPriceMethod->setAccessible(true); $sortedBooks = $sortByPriceMethod->invoke( $realClass, $this->jsonBody->books ); $this->assertEquals(12.95, $sortedBooks[0]->price); $this->assertEquals(17.99, $sortedBooks[4]->price); } } Testing Second Method with Reflection 6 LOC
  23. * The second method I want to test */ private

    static function sortBooksByPrice($books) { //usort is going to modify $books by reference, //so lets copy it to prevent side effects $sortedBooks = $books; usort($sortedBooks, function($a, $b) { /** * compare decimal price */ return $a->price >= $b->price; }); return $sortedBooks; } public function testSortBooksByPrice() { $realClass = new Render(); $sortByPriceMethod = new ReflectionMethod( $realClass, 'sortBooksByPrice' ); $sortByPriceMethod->setAccessible(true); $sortedBooks = $sortByPriceMethod->invoke( $realClass, $this->jsonBody->books ); $this->assertEquals(12.95, $sortedBooks[0]->price); $this->assertEquals(17.99, $sortedBooks[4]->price); }
  24. None
  25. private $books, $jsonBody; public static function sortBooksByIsbn($books) { //usort is

    going to modify $books by reference, //so lets copy it to prevent side effects $sortedBooks = $books; usort($sortedBooks, function($a, $b) { /** * sort by ISBN string which might being with zero */ return strcmp($a->isbn, $b->isbn); }); return $sortedBooks; } public function testSortBooksByISBN() { $sortedBooks = Render::sortBooksByIsbn($this->jsonBody- >books); $this->assertEquals('9780544107717', $sortedBooks[0]->isbn); $this->assertEquals('9781616203535', $sortedBooks[4]->isbn); Third Method I want to test Changed to public No side effect risk
  26. //so lets copy it to prevent side effects $sortedBooks =

    $books; usort($sortedBooks, function($a, $b) { /** * sort by ISBN string which might being with zero */ return strcmp($a->isbn, $b->isbn); }); return $sortedBooks; } public function testSortBooksByISBN() { $sortedBooks = Render::sortBooksByIsbn($this->jsonBody->books); $this->assertEquals('9780544107717', $sortedBooks[0]->isbn); $this->assertEquals('9781616203535', $sortedBooks[4]->isbn); } } Testing Third Method without Reflection 3 LOC
  27. public static function sortBooksByIsbn($books) { //usort is going to modify

    $books by reference, //so lets copy it to prevent side effects $sortedBooks = $books; usort($sortedBooks, function($a, $b) { /** * sort by ISBN string which might being with zero */ return strcmp($a->isbn, $b->isbn); }); return $sortedBooks; } public function testSortBooksByISBN() { $sortedBooks = Render::sortBooksByIsbn($this->jsonBody->books); $this->assertEquals('9780544107717', $sortedBooks[0]->isbn); $this->assertEquals('9781616203535', $sortedBooks[4]->isbn); } }
  28. None
  29. Visibility • Transformation cases • Can remove or replace $this

    for self in method • Not hitting the database • No side effects or • Calculated side effects • Add object type hints • Can’t do it another way Reflection Use Both
  30. Solving Database Coupling • Each test does not have to

    have an empty DB: • Refresh from DB from master as often as ‘reasonable’ • unique() will prevent column restrictions and hash collisions • Hacks are OK, at the level “some tests > no tests”
  31. class ServiceTest extends \PHPUnit_Framework_TestCase { private $customer; public function setUp()

    { $this->customer = self::mockCustomer(); } public static function mockCustomer() { //build up a test object $customer = new \stdClass(); $customer->name = uniqid('John Test'); $customer->email = uniqid('test').'@test.com'; $customer->address = rand(1,5000).' test street'; $cities = ['Roswell', 'Austin', 'Memphis', 'San Jose']; $customer->city = $cities[array_rand($cities)]; $states = ['NC', 'TX', 'VA', 'CT']; $customer->state = $states[array_rand($states)]; $customer->zip = rand(10000,70000); return $customer; } public function testUpdateCustomer() { return ; $service = new Service($sandboxKey = 1212312, $sandboxPassword = 5555 $id = $service->updateCustomer($this->customer); $this->assertGreaterThan(0, $id); Fixes Hash problems Makes debug easier for relational records Strings should have different shapes in UI Semi-unique response for each request
  32. * @return string */ public static function generateValidCreditCardNumber($leader = 4)

    { //set to amex 15 digit if prefix is 3. otherwise its 16 $totalDigits = (substr($leader, 0, 1) == 3) ? 14 : 15; $forwardCardDigits = str_split($leader); // build out the rest of the digits, except for checksum (last digit) for ($i = 0; $i < $totalDigits - strlen($leader); $i++) { $forwardCardDigits[] = rand(0,9); } $reverseCardDigits = array_reverse($forwardCardDigits); $checksum = 0; foreach ($reverseCardDigits as $key => $value) { if ($key % 2 === 0) { $value = 2 * $value; } if ($value > 9) { $checksum += $value - 9; } else { $checksum += $value; } } //get the least significant digit for the checksum $tmp = str_split($checksum); $lsd = (int) end($tmp); $checksumCompliment = $lsd === 0 ? 0 : 10 - $lsd; return implode(null, $forwardCardDigits) . $checksumCompliment; }
  33. Mock* Factories • Build a mock factory for every model

    • Build factories that chain mock object relationships together • Test complex things, start small User Payment Method Account Location Booking Statement Batch Statement Sales Order Invoice
  34. DB Coupling: Hacks DB’s don’t have to be refreshed between

    every test. Some things can be left in the DB by the test. Test code can be written to minimize leftovers.
  35. { private $books, $jsonBody; public function testFindRecordLeavesAMess() { $insertIds =

    []; $this->pdo->insert('some record'); $insertIds[] = $this->pdo->getLastInsertId(); $this->pdo->insert('some record2'); $insertIds[] = $this->pdo->getLastInsertId(); ... $records = My\Service::findRecord('my report'); $found = false; foreach ($records as $record) { if ($record->someCondition() !== false) { $found = true; } } if (! $found) { $this->fail('did not find the record we expected'); } foreach ($insertIds as $id) { $this->pdo->delete($id); } } Test will leave records any time it fails
  36. { private $books, $jsonBody; public function testFindRecordTeardownFirst() { $insertIds =

    []; $this->pdo->insert('some record'); $insertIds[] = $this->pdo->getLastInsertId(); $this->pdo->insert('some record2'); $insertIds[] = $this->pdo->getLastInsertId(); ... $records = My\Service::findRecord('my report'); $found = false; foreach ($records as $record) { if ($record->someCondition() !== false) { $found = true; } } foreach ($insertIds as $id) { $this->pdo->delete($id); } if (! $found) { $this->fail('did not find the record we expected'); } } This test will clean up before failing
  37. Network Services • Wiremock & Wiremock PHP • Works at

    the network layer like Man-in-middle attack • Intercepts service requests • Returns mock data or routes to a service provider, based on your pre-defined logic • http://wiremock.org/ • https://github.com/rowanhill/wiremock-php
  38. Do I need CI?

  39. switch (“Do I need CI?”) { case Agile Team: return

    ‘Yes’; case count(Team) < ~n: return ‘No’; case count(Team) = ~n: return ‘Maybe’; case count(Team) > ~n: return ‘Yes’; default: return ‘Yes’; }
  40. How many people is n? • One person: I have

    self discipline. • Two people: I know to blame the other one. • Three people: Things were fine until a snake entered the garden.
  41. … feature speed increases. … regressions can stop. … iteration

    and feedback cycles can increase. In exchange for a one-time, but massive PIA setup… Talk on the how tomorrow at 11am
  42. With the addition of CI, each test becomes a contract,

    between the feature and the maintainers of the application. It says, ‘This feature was important enough to have a test written for it. This contract forbids you from (accidentally) breaking said feature.’ Regressions
  43. “It doesn’t matter if it works or not. Just make

    the tests pass so we can deploy.” Then Again… …its not for everyone.
  44. Try CI with training wheels first. This is Travis CI

  45. A real project in Bamboo

  46. Information Radiators are a part of CI

  47. Code Coverage

  48. <phpunit bootstrap="vendor/autoload.php" colors="true" coverage-html="true"> <testsuites> <testsuite name="SampleTests"> <directory>tests</directory> </testsuite> </testsuites>

    <logging> <log type="junit" target="junit.xml"/> <!-- <log type="json" target="testlog.json"/> --> <log type="coverage-html" target="codeCoverage" charset="UTF-8" yui="true" highlight="true" lowUpperBound="50" highLowerBound="80"/> </logging> </phpunit> Load xdebug extension and add this block to phpunit.xml https://github.com/matt-land/test-samples results appear in /codeCoverage
  49. Ice Cream Cones & Behavior Driven Development

  50. None
  51. None
  52. How does it get this way?

  53. • Store the data in persistence • Load the UI

    • Manipulate the UI • Bounce the UI, since first time was just jQuery • Verify the side effects in persistence Typical UI Test
  54. • Store the data in persistence • Load the UI

    • Manipulate the UI • Bounce the UI, since first time was just jQuery • Verify the side effects in persistence Tech Debt The test is doing too much, or test the service separately Because our logic is everywhere Because the DB is coupled Because our logic is everywhere Typical UI Test
  55. • Store the data in persistence • Load the UI

    • Manipulate the UI • Bounce the UI, since first time was just jQuery • Verify the side effects in persistence Tech Debt The test is doing too much, or test the service separately Because our logic is everywhere Because the DB is coupled Because our logic is everywhere The result is each test is very slow. Typical UI Test
  56. • Store the data in persistence • Load the UI

    • Manipulate the UI • Bounce the UI, since first time was just jQuery • Verify the side effects in persistence Tech Debt The test is doing too much, or test the service separately Because our logic is everywhere Because the DB is coupled Because our logic is everywhere Its OK! Tests > No Tests, and tech debt is fixable. But its only OK up to a point. Typical UI Test
  57. Intermittent Failure To Launch

  58. • 1:45pm - start the build again • 2:00pm -

    notice build hasn’t passed yet today • 2:10pm - bounce selenium grid for luck • 2:30pm - look in hip chat debug room for screenshots • 2:40pm - deploy time is coming. Last chance to go green in time • 2:55pm - build failure. curse violently. Devbot mocks me • 3:10pm - bow out of the deployment • 4:05pm - finally a green build. !Freeze the build, indefinitely • 6:45pm - DevOps is back from kid’s swim practice • Successfully Deploy!
  59. Intermittent test failure smell • Blame the test • Blame

    the CI environment • Blame CI environment performance • Blame the JavaScript plugins (jQuery whatever) • Your MVC code is doing too damn much • Code actually fails a large percentage of the time in prod. You just hadn’t noticed.
  60. My Worst UI Test

  61. None
  62. Learn from this • Selenium is a whole other level

    of difficulty • Test on hotel Wifi, see if you can pass • Remove side effects from controllers • Sending Emails • Uploads to S3 • Calls to push data to other SaaS’s
  63. Its not me, its the environment

  64. Testing the Untestable Code Samples: github.com/matt-land Slides: speakerdeck.com/mattland/ linkedin.com/in/matthewland https://joind.in/15548

  65. Bonus

  66. Sticky Bits I run tests both ways. In Vagrant (headless)

    and in host OS (interactive) Both are super useful to my workflow: • In the host OS for writing and debugging a broken condition • In the VM for running a suite before opening my pull request
  67. Sticky Bits (2) Multiple Ant targets (smoke vs full vs

    regression) balance • speed of quick feedback • full regression coverage • (don’t trust anyone) Code coverage plug-ins are invalid in the scope for integration level tests.
  68. Sticky Bits (3) • IFRAME->IFRAME is one way trip •

    Write a dedicated testsuite API for Datepicker() • Expected, Actual, Whoops-Message • Explicitly wait for _everything_ • Disable tests just like C++ by underscore prefixing • Fail as fast as possible (Temporarily disable long tests higher in the class)
  69. Sticky Bits (4) • Feature Toggle Iteration • Cookie up

    with 404 • add controller/action/behavior ids to the dom • Use identical IDs when FF’ing so the test can follow • Request/Factory/Response Mock Factory • Watch for _test nerving in pull requests