Learn To Test Like A Grumpy Programmer

Learn To Test Like A Grumpy Programmer

Slides from the workshop I gave at Symfony Live Berlin on October 26, 2017

27601bca8f38e75cbcf9d2dc843f0b32?s=128

Chris Hartjes

October 26, 2017
Tweet

Transcript

  1. Become A Be!er, Grumpier Tester

  2. Today — TDD as a design tool — The role

    of tests in your application — Testing building blocks — Code katas to build testing skills — Difficult language constructs to test — Code coverage — Mutation testing
  3. But isn't TDD about tests?!?

  4. But isn't TDD about tests?!? Tests are just part of

    it
  5. Test-DRIVEN Development

  6. You use tests to DRIVE the design of your application

  7. The Grumpy Way Have an idea Write a test like

    everything works Write code until the test passes Repeat until you are internet- famous
  8. None
  9. TDD As Design Tool We use the tests to help

    us build out a collection of loosely-coupled modules that accomplish well-defined tasks
  10. The role of tests 5 factors courtesy of Sarah Mei

    https://www.devmynd.com/blog/five-factor-testing/
  11. The role of tests Verify the code is working correctly

  12. The role of tests Prevent future regressions

  13. The role of tests Document the code's behaviour

  14. The role of tests Provide design guidance

  15. The role of tests Support refactoring

  16. Testing Building Blocks

  17. Testing Building Blocks Assertions Created by the folks who invented

    NUnit style testing in the Java world
  18. Testing Building Blocks Assertions Revolves around a very simple concept:

    your test is designed to prove that the result is as you expected it to be
  19. Testing Building Blocks Assertions $this->assertTrue(<something>); $this->assertFalse(<something>); $this->assertEquals($foo, $bar);

  20. Testing Building Blocks Assertions These cover 99% of everything I've

    ever needed to assert
  21. TDD As Design Tool Code Kata #1 FizzBuzz

  22. TDD As Design Tool Code Kata #1 — take a

    collection of positive integers — iterate over the collection — replace all integers % 3 with 'Fizz' — replace all integers % 5 with 'Buzz' — replace all integers % 5 and 3 with 'FizzBuzz'
  23. TDD As Design Tool Code Kata #1 AAA Testing Pa!ern

  24. TDD As Design Tool Code Kata #1 Arrange

  25. TDD As Design Tool Code Kata #1 Act

  26. TDD As Design Tool Code Kata #1 Assert

  27. TDD As Design Tool Code Kata #2 Build a Magic

    The Gathering deck list analyzer
  28. Code Kata #2 Parse a deck list submi!ed as an

    array Sort the items by number of cards and then alphabetical
  29. Code Kata #2 Each line consists of [Number of cards][optional

    'x' or 'X'] [Name Of Card] — [Name of Card] is alphabetical characters, commas and apostophres
  30. Code Kata #2 Example: 2X Taiga 1 Forest 3 Grove

    of the Burnwillows 4 Wasteland 1x Verdant Catacombs Becomes 4x Wasteland 3x Grove of the Burnwillows
  31. Testing Building Blocks Doubles

  32. Testing Building Blocks (You probably call them mocks)

  33. Testing Building Blocks They are used when you need a

    dependency for a test and don't want to use the real thing
  34. Testing Building Blocks Doubles — Fakes — Dummies — Stubs

    — Mocks
  35. Testing Building Blocks Fake A Fake is when you create

    your own version of a dependency that has the same functionality class FakeMailer { function validateEmail($emailAddress) { // Code using regex to verify addresses } function sendMail($emailAddress, $msg) { return true; } }
  36. Testing Building Blocks Fake — Interesting alternative to extensive setup

    of test doubles
  37. Testing Building Blocks Dummy A Dummy is when you create

    a version of a dependency that has the same class signature but has no functionality
  38. Testing Building Blocks Dummy $double = $this->prophesize(SpamCheckerService::class); $spamCheckerService = $double->reveal();

  39. Testing Building Blocks Stub A Stub is when you create

    a version of a dependency that has the same class signature and the methods your test needs defined
  40. Testing Building Blocks Stub $double = $this->prophesize(SpamCheckerService::class); $double->process(\Prophecy\Argument::type('string')); $spamCheckerService =

    $double->reveal;
  41. Testing Building Blocks Mock A Mock is when you create

    a version of a depedency that has the same class signature, the methods your test needs defined, and methods return values
  42. Testing Building Blocks Mock $double = $this->prophesize(SpamCheckerService::class); $response = '{"success":true,"score":"7.9"}';

    $double->process(\Prophecy\Argument::type('string')) ->willReturn($response); $spamCheckerService = $double->reveal;
  43. Testing Building Blocks So what double should I use?

  44. Testing Building Blocks When should I use a Fake instead

    of a Mock?
  45. Testing Building Blocks Unfortunately it depends

  46. Testing Building Blocks Some guidelines

  47. Testing Building Blocks Some guidelines Always use the real thing

    if you can Work hard to keep your doubles up to date
  48. Testing Building Blocks Always use the real thing if you

    can Doubles increase the size of your Arrange step
  49. Testing Building Blocks Work hard to keep your doubles up

    to date Use doubling tools that help you!
  50. Testing Building Blocks Work hard to keep your doubles up

    to date Prophecy is currently the best choice
  51. Testing Building Blocks Work hard to keep your doubles up

    to date Prophecy won't let you create doubles with method calls that don't exist on the dependency
  52. Code Kata #3 Catching spam

  53. Code Kata #3 Create an object that submits a message

    to h!p:// spamcheck.postmarkapp.com Processes the response on whether it's spam or not
  54. Code Kata #3 But you can't speak to the actual

    service!
  55. Code Kata #3 GO GO GO!

  56. Code Kata #3 Implement code for handling: — '{"success":true,"score":"3.1"}' —

    '{"success":false,"message":""}'
  57. Code Coverage

  58. Code Coverage — Tool for looking at what code is

    being run by your tests
  59. Code Coverage — Built into PHPUnit, requires XDebug to be

    installed
  60. Mutation Testing

  61. Mutation Testing — Randomly changes your code ("mutating") — Looking

    for tests that pass even when code is mutated
  62. Mutation Testing — Sometimes the mutations are false-positives! — Requires

    very-specific testing scenarios to have a "high score"
  63. Mutation Testing [fit] Infection

  64. Mutation Testing — interacts with PHPUnit or PHPSpec — generates

    a report
  65. Mutation Testing 861 mutations were generated: 383 mutants were killed

    414 mutants were not covered by tests 64 covered mutants were not detected 0 time outs were encountered Metrics: Mutation Score Indicator (MSI): 44% Mutation Code Coverage: 52% Covered Code MSI: 86%
  66. Difficult Language Constructs

  67. Difficult Language Constructs Global objects

  68. Difficult Language Constructs Global objects They lurk everywhere and anywhere

  69. Difficult Language Constructs Global objects They lurk everywhere and anywhere

    Adds extra work to the Arrange step
  70. Difficult Language Constructs Global objects Good practice - refactor to

    configuration objects Allows over-riding them in your arrange steps
  71. Difficult Language Constucts Global objects

  72. Difficult Langage Constructs Service Locators They are like global variables

    with methods a!ached to them!
  73. Difficult Language Constructs Service Locators class DashboardController extends BaseController {

    public function showSpeakerProfile() { /** * Local reference to speakers application service. * * This should be injected instead of using service location but there's currently a * "conflict" between Controller as Services and our custom ControllerResolver that injects the Application * container. * * @var Speakers $speakers */ $speakers = $this->service('application.speakers'); try { $profile = $speakers->findProfile(); /** @var CallForProposal $cfp */ $cfp = $this->service('callforproposal'); return $this->render('dashboard.twig', [ 'profile' => $profile, 'cfp_open' => $cfp->isOpen(), ]); } catch (NotAuthenticatedException $e) { return $this->redirectTo('login'); } }
  74. Difficult Language Constructs Service Locators $speakers = $this->service('application.speakers');

  75. Difficult Language Constructs Service Locators class DashboardControllerTest extends \PHPUnit\Framework\TestCase {

    use GeneratorTrait; /** * Test that the index page returns a list of talks associated * with a specific user and information about that user as well * * @test */ public function indexDisplaysUserAndTalks() { $app = new Application(BASE_PATH, Environment::testing()); $app['session.test'] = true; // Set things up so Sentry believes we're logged in $user = m::mock('StdClass'); $user->shouldReceive('id')->andReturn(1); $user->shouldReceive('getId')->andReturn(1); $user->shouldReceive('hasAccess')->with('admin')->andReturn(true); // Create a test double for Sentry $sentry = m::mock(Sentry::class); $sentry->shouldReceive('check')->times(3)->andReturn(true); $sentry->shouldReceive('getUser')->andReturn($user); $app['sentry'] = $sentry; $app['callforproposal'] = m::mock(CallForProposal::class); $app['callforproposal']->shouldReceive('isOpen')->andReturn(true); // Create a test double for a talk in profile $talk = m::mock('StdClass'); $talk->shouldReceive('title')->andReturn('Test Title'); $talk->shouldReceive('id')->andReturn(1); $talk->shouldReceive('type', 'category', 'created_at'); // Create a test double for profile $profile = m::mock('StdClass'); $profile->shouldReceive('name')->andReturn('Test User'); $profile->shouldReceive('photo', 'company', 'twitter', 'airport', 'bio', 'info', 'transportation', 'hotel'); $profile->shouldReceive('talks')->andReturn([$talk]); $speakerService = m::mock('StdClass'); $speakerService->shouldReceive('findProfile')->andReturn($profile); $app['application.speakers'] = $speakerService; ob_start(); $app->run(); // Fire before handlers... boot... ob_end_clean(); // Instantiate the controller and run the indexAction $controller = new DashboardController(); $controller->setApplication($app); $response = $controller->showSpeakerProfile(); $this->assertContains('Test Title', (string) $response); $this->assertContains('Test User', (string) $response); } }
  76. Difficult Language Constructs Service Locators class DashboardControllerTest extends \PHPUnit\Framework\TestCase {

    use GeneratorTrait; /** * Test that the index page returns a list of talks associated * with a specific user and information about that user as well * * @test */ public function indexDisplaysUserAndTalks() { $app = new Application(BASE_PATH, Environment::testing()); /** Other arrange work goes here **/ // Create a test double for a talk in profile $talk = m::mock('StdClass'); $talk->shouldReceive('title')->andReturn('Test Title'); $talk->shouldReceive('id')->andReturn(1); $talk->shouldReceive('type', 'category', 'created_at'); // Create a test double for profile $profile = m::mock('StdClass'); $profile->shouldReceive('name')->andReturn('Test User'); $profile->shouldReceive('photo', 'company', 'twitter', 'airport', 'bio', 'info', 'transportation', 'hotel'); $profile->shouldReceive('talks')->andReturn([$talk]); $speakerService = m::mock('StdClass'); $speakerService->shouldReceive('findProfile')->andReturn($profile); $app['application.speakers'] = $speakerService; /** Act and assert steps to follow *// } }
  77. Diffuclt Language Constructs Service Locators Good practice seems to be

    think hard about what you're doing and minimize their use
  78. Difficult Language Constructs Tightly coupled-dependencies

  79. Difficult Languge Constructs Tightly coupled-dependencies class SimpleExample { function doSomething($data)

    { $foo = new Foo; $bar = new Bar; $result = $foo->mangle($bar->transform($data)); return $result; } } $example = new SimpleExample; $data = [1, 2, 3, 4, 5]; echo $exmple->doSomething($data);
  80. Difficult Languge Constructs Tightly coupled-dependencies class SimpleExample { public $foo;

    public $bar; public function __constuct($foo, $bar) { $this->foo = $foo; $this->bar = $bar; } public function doSomething($data) { $result = $this->foo->mangle($this->bar->transform($data)); return $result; } } $foo = new Foo; $bar = new Bar; $example = new SimpleExample($foo, $bar); $data = [1, 2, 3, 4, 5]; echo $exmple->doSomething($data);
  81. Difficult Language Constructs Tightly Coupled Dependencies Good practice is to

    refactor towards dependency injection
  82. Difficult Language Constructs Tightly Coupled Dependencies — Service locators —

    Dependency injection containers
  83. Current Grumpy Practices for be!er organized tests

  84. Current Grumpy Practices Create Testing Helpers

  85. Current Grumpy Practices public function youCannotEditSomeoneElsesProfile() { $this->app['sentinel'] = $this->createSentinelWithLoggedInUser();

    ob_start(); $this->app->run(); ob_end_clean(); $req = m::mock(Request::class); $req->shouldReceive('get')->with('id')->andReturn(2); $controller = new ProfileController(); $controller->setApp($this->app); $response = $controller->editAction($req); $this->assertContains( 'Redirecting to /dashboard', $response->getContent(), "User was not redirected to dashboard after trying to edit someone else's profile" ); }
  86. Current Grumpy Practices public function createSentinelWithLoggedInUser() { $sentinel = m::mock(SentinelWrapper::class);

    $sentinel->shouldReceive('check')->andReturn(true); $sentinel->shouldReceive('getUser')->andReturn($this->createUser()); return $sentinel; }
  87. Current Grumpy Practices Don't ever forget that tests are just

    code
  88. Current Grumpy Practices Good coding practices result in good tests

  89. Current Grumpy Practices Try and avoid the file system

  90. Current Grumpy Practices public function createImageFile() { // Create a

    virtual file system and image file vfsStreamWrapper::register(); $root = vfsStream::newDirectory('images'); vfsStreamWrapper::setRoot($root); $root->addChild(vfsStream::newFile('image.jpg')); return new UploadedFile(vfsStream::url('images/image.jpg'), 'image.jpg'); }
  91. Current Grumpy Practices Virtual file systems leave no messes to

    clean up
  92. Current Grumpy Practices In-memory databases

  93. Current Grumpy Practices // Override things so that Spot2 is

    using in-memory tables $cfg = new \Spot\Config; $cfg->addConnection('sqlite', [ 'dbname' => 'sqlite::memory', 'driver' => 'pdo_sqlite', ]); $spot = new \Spot\Locator($cfg); unset($this->app['spot']); $this->app['spot'] = $spot;
  94. Current Grumpy Practices Nice compromise when you need an actual

    database
  95. Current Grumpy Practices Encourages use of data mappers and ORM's

  96. Current Grumpy Practices Like a VFS, clean up happens a!er

    the tests are run
  97. Current Grumpy Practices Data Providers /** * @test * @dataProvider

    invalidTalkTitles * @expectedException \OpenCFP\Domain\Talk\InvalidTalkSubmissionException * @expectedExceptionMessage title */ public function it_guards_that_title_is_appropriate_length($title) { TalkSubmission::fromNative(['title' => $title]); }
  98. Current Grumpy Practices Data Providers public function invalidTalkTitles() { return

    [ [''], ['String over one-hundred characters long: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc vel placerat nulla. Nunc orci aliquam.'], ]; }
  99. Current Grumpy Practices Data Providers /** * @test * @dataProvider

    specializedResponseExamples */ public function it_has_helpers_to_send_specialized_responses($type, $expectedStatus, $expectedDefaultMessage) { $methodName = "respond{$type}"; $response = $this->sut->$methodName(); $this->assertEquals($expectedStatus, $response->getStatusCode()); $this->assertContains($expectedDefaultMessage, $response->getContent()); }
  100. Current Grumpy Practices Data Providers public function specializedResponseExamples() { return

    [ ['BadRequest', 400, 'Bad request'], ['Unauthorized', 401, 'Unauthorized'], ['Forbidden', 403, 'Forbidden'], ['NotFound', 404, 'Resource not found'], ['InternalError', 500, 'Internal server error'], ]; }
  101. Current Grumpy Practices Data Providers Their use encourages more generic

    tests
  102. Current Grumpy Practices Data Providers Extend the provider to include

    more data instead of slightly-different tests
  103. Test-A!er Skills

  104. Test-A!er Skills Identifying Dependencies

  105. Test-A!er Skills /** * Controller action for viewing a specific

    talk * * @param Request $req * @return mixed */ public function viewAction(Request $req) { if (!$this->service('sentinel')->check()) { return $this->redirectTo('login'); } try { $speakers = $this->service('application.speakers'); $id = filter_var($req->get('id'), FILTER_VALIDATE_INT); $talk = $speakers->getTalk($id); } catch (NotAuthorizedException $e) { return $this->redirectTo('dashboard'); } return $this->render('talk/view.twig', compact('id', 'talk')); }
  106. Test-A!er Skills Testing a non-authorized user trying to view a

    talk? if (!$this->service('sentinel')->check()) { return $this->redirectTo('login'); }
  107. Test-A!er Skills public function viewKicksOutUsersWhoAreNotLoggedIn() { // We need a

    Sentinel user who is not logged in unset($this->app['sentinel']); $this->app['sentinel'] = $this->createNotLoggedInUser(); $controller = new TalkController(); $controller->setApplication($this->app); $this->assertContains( 'Redirecting to /login', $controller->viewAction($this->req)->getContent(), 'Non-logged in user can view a talk' ); }
  108. Test-A!er Skills try { $speakers = $this->service('application.speakers'); $id = filter_var($req->get('id'),

    FILTER_VALIDATE_INT); $talk = $speakers->getTalk($id); } catch (NotAuthorizedException $e) { return $this->redirectTo('dashboard'); } return $this->render('talk/view.twig', compact('id', 'talk'));
  109. Test-A!er Skills public function viewRendersTalkForLoggedInUser() { list($talk, $talk_id) = $this->createTalk();

    // Create a double for our speaker object $application_speakers = m::mock('\stdClass'); $application_speakers->shouldReceive('getTalk')->with($talk_id)->andReturn($talk); $this->app['application.speakers'] = $application_speakers; // Tell our request object what the ID of the talk is $this->req->shouldReceive('get')->with('id')->andReturn($talk_id); $controller = new TalkController(); $controller->setApplication($this->app); $this->assertContains( '<!-- id: talk/view -->', $controller->viewAction($this->req)->getContent(), 'TalkController::viewAction did not correctly render view' ); }
  110. Test-A!er Skills Comment your tests so new folks understand what's

    happening!
  111. Test-A!er Skills HTML comments make searching for specific output easy

    $this->assertContains( '<!-- id: talk/view -->', $controller->viewAction($this->req)->getContent(), 'TalkController::viewAction did not correctly render view' );
  112. Test-A!er Skills Sometimes the code needs to change to be

    more testable
  113. Test-A!er Skills Sometimes the code needs to change to be

    more testable Consider making your application "environment-aware"
  114. Test-A!er Skills Environment Aware Code Server-level environment variables

  115. Test-A!er Skills Environment Aware Code Configuration values keyed to environments

  116. The END

  117. Questions? chartjes@grumpy-learning.com @grmpyprogrammer