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

Testing the Untestable Zendcon 2015

Matt Land
October 21, 2015

Testing the Untestable Zendcon 2015

Matt Land

October 21, 2015
Tweet

More Decks by Matt Land

Other Decks in Programming

Transcript

  1. 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
  2. Getting Started: Just Do It Noise: • PHPUnit • Asserts

    • Composer/Autoloaders • Namespaces • Coding Standards • Continuous Integration • Best Practices • Mocks Not Noise: • Writing Tests • Passing Tests
  3. 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
  4. 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
  5. 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
  6. 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
  7. 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
  8. <?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
  9. } 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
  10. 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); }
  11. /** * @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::
  12. */ 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
  13. * 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); }
  14. 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
  15. //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
  16. 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); } }
  17. 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
  18. 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”
  19. 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
  20. * @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; }
  21. 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
  22. 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.
  23. { 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
  24. { 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
  25. 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
  26. 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’; }
  27. 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.
  28. … 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
  29. 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
  30. “It doesn’t matter if it works or not. Just make

    the tests pass so we can deploy.” Then Again… …its not for everyone.
  31. <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
  32. • 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
  33. • 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
  34. • 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
  35. • 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
  36. • 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!
  37. 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.
  38. 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
  39. 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
  40. 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.
  41. 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)
  42. 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