$30 off During Our Annual Pro Sale. View Details »

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

Chris Hartjes

October 26, 2017
Tweet

More Decks by Chris Hartjes

Other Decks in Technology

Transcript

  1. Become A Be!er,
    Grumpier Tester

    View Slide

  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

    View Slide

  3. But isn't TDD about tests?!?

    View Slide

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

    View Slide

  5. Test-DRIVEN Development

    View Slide

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

    View Slide

  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

    View Slide

  8. View Slide

  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

    View Slide

  10. The role of tests
    5 factors courtesy of Sarah Mei
    https://www.devmynd.com/blog/five-factor-testing/

    View Slide

  11. The role of tests
    Verify the code is working correctly

    View Slide

  12. The role of tests
    Prevent future regressions

    View Slide

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

    View Slide

  14. The role of tests
    Provide design guidance

    View Slide

  15. The role of tests
    Support refactoring

    View Slide

  16. Testing Building Blocks

    View Slide

  17. Testing Building Blocks
    Assertions
    Created by the folks who invented NUnit style testing in
    the Java world

    View Slide

  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

    View Slide

  19. Testing Building Blocks
    Assertions
    $this->assertTrue();
    $this->assertFalse();
    $this->assertEquals($foo, $bar);

    View Slide

  20. Testing Building Blocks
    Assertions
    These cover 99% of everything I've ever needed to assert

    View Slide

  21. TDD As Design Tool
    Code Kata #1
    FizzBuzz

    View Slide

  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'

    View Slide

  23. TDD As Design Tool
    Code Kata #1
    AAA Testing Pa!ern

    View Slide

  24. TDD As Design Tool
    Code Kata #1
    Arrange

    View Slide

  25. TDD As Design Tool
    Code Kata #1
    Act

    View Slide

  26. TDD As Design Tool
    Code Kata #1
    Assert

    View Slide

  27. TDD As Design Tool
    Code Kata #2
    Build a Magic The Gathering deck
    list analyzer

    View Slide

  28. Code Kata #2
    Parse a deck list submi!ed as an array
    Sort the items by number of cards and then alphabetical

    View Slide

  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

    View Slide

  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

    View Slide

  31. Testing Building Blocks
    Doubles

    View Slide

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

    View Slide

  33. Testing Building Blocks
    They are used when you need a
    dependency for a test and don't
    want to use the real thing

    View Slide

  34. Testing Building Blocks
    Doubles
    — Fakes
    — Dummies
    — Stubs
    — Mocks

    View Slide

  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;
    }
    }

    View Slide

  36. Testing Building Blocks
    Fake
    — Interesting alternative to extensive setup of test
    doubles

    View Slide

  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

    View Slide

  38. Testing Building Blocks
    Dummy
    $double = $this->prophesize(SpamCheckerService::class);
    $spamCheckerService = $double->reveal();

    View Slide

  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

    View Slide

  40. Testing Building Blocks
    Stub
    $double = $this->prophesize(SpamCheckerService::class);
    $double->process(\Prophecy\Argument::type('string'));
    $spamCheckerService = $double->reveal;

    View Slide

  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

    View Slide

  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;

    View Slide

  43. Testing Building Blocks
    So what double should I use?

    View Slide

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

    View Slide

  45. Testing Building Blocks
    Unfortunately it depends

    View Slide

  46. Testing Building Blocks
    Some guidelines

    View Slide

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

    View Slide

  48. Testing Building Blocks
    Always use the real thing if you
    can
    Doubles increase the size of your Arrange step

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  52. Code Kata #3
    Catching spam

    View Slide

  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

    View Slide

  54. Code Kata #3
    But you can't speak to the actual service!

    View Slide

  55. Code Kata #3
    GO GO GO!

    View Slide

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

    View Slide

  57. Code Coverage

    View Slide

  58. Code Coverage
    — Tool for looking at what code is being run by your
    tests

    View Slide

  59. Code Coverage
    — Built into PHPUnit, requires XDebug to be installed

    View Slide

  60. Mutation Testing

    View Slide

  61. Mutation Testing
    — Randomly changes your code ("mutating")
    — Looking for tests that pass even when code is
    mutated

    View Slide

  62. Mutation Testing
    — Sometimes the mutations are false-positives!
    — Requires very-specific testing scenarios to have a
    "high score"

    View Slide

  63. Mutation Testing
    [fit] Infection

    View Slide

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

    View Slide

  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%

    View Slide

  66. Difficult Language Constructs

    View Slide

  67. Difficult Language Constructs
    Global objects

    View Slide

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

    View Slide

  69. Difficult Language Constructs
    Global objects
    They lurk everywhere and anywhere
    Adds extra work to the Arrange step

    View Slide

  70. Difficult Language Constructs
    Global objects
    Good practice - refactor to configuration objects
    Allows over-riding them in your arrange steps

    View Slide

  71. Difficult Language Constucts
    Global objects

    View Slide

  72. Difficult Langage Constructs
    Service Locators
    They are like global variables with methods a!ached to
    them!

    View Slide

  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');
    }
    }

    View Slide

  74. Difficult Language Constructs
    Service Locators
    $speakers = $this->service('application.speakers');

    View Slide

  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);
    }
    }

    View Slide

  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 *//
    }
    }

    View Slide

  77. Diffuclt Language Constructs
    Service Locators
    Good practice seems to be think hard about what you're
    doing and minimize their use

    View Slide

  78. Difficult Language Constructs
    Tightly coupled-dependencies

    View Slide

  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);

    View Slide

  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);

    View Slide

  81. Difficult Language Constructs
    Tightly Coupled Dependencies
    Good practice is to refactor towards dependency
    injection

    View Slide

  82. Difficult Language Constructs
    Tightly Coupled Dependencies
    — Service locators
    — Dependency injection containers

    View Slide

  83. Current Grumpy Practices
    for be!er organized tests

    View Slide

  84. Current Grumpy Practices
    Create Testing Helpers

    View Slide

  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"
    );
    }

    View Slide

  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;
    }

    View Slide

  87. Current Grumpy Practices
    Don't ever forget that tests are just
    code

    View Slide

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

    View Slide

  89. Current Grumpy Practices
    Try and avoid the file system

    View Slide

  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');
    }

    View Slide

  91. Current Grumpy Practices
    Virtual file systems leave no
    messes to clean up

    View Slide

  92. Current Grumpy Practices
    In-memory databases

    View Slide

  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;

    View Slide

  94. Current Grumpy Practices
    Nice compromise when you need
    an actual database

    View Slide

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

    View Slide

  96. Current Grumpy Practices
    Like a VFS, clean up happens a!er
    the tests are run

    View Slide

  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]);
    }

    View Slide

  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.'],
    ];
    }

    View Slide

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

    View Slide

  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'],
    ];
    }

    View Slide

  101. Current Grumpy Practices
    Data Providers
    Their use encourages more generic tests

    View Slide

  102. Current Grumpy Practices
    Data Providers
    Extend the provider to include more data instead of
    slightly-different tests

    View Slide

  103. Test-A!er Skills

    View Slide

  104. Test-A!er Skills
    Identifying Dependencies

    View Slide

  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'));
    }

    View Slide

  106. Test-A!er Skills
    Testing a non-authorized user trying to view a talk?
    if (!$this->service('sentinel')->check()) {
    return $this->redirectTo('login');
    }

    View Slide

  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'
    );
    }

    View Slide

  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'));

    View Slide

  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(
    '',
    $controller->viewAction($this->req)->getContent(),
    'TalkController::viewAction did not correctly render view'
    );
    }

    View Slide

  110. Test-A!er Skills
    Comment your tests so new folks
    understand what's happening!

    View Slide

  111. Test-A!er Skills
    HTML comments make searching for specific output easy
    $this->assertContains(
    '',
    $controller->viewAction($this->req)->getContent(),
    'TalkController::viewAction did not correctly render view'
    );

    View Slide

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

    View Slide

  113. Test-A!er Skills
    Sometimes the code needs to
    change to be more testable
    Consider making your application "environment-aware"

    View Slide

  114. Test-A!er Skills
    Environment Aware Code
    Server-level environment variables

    View Slide

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

    View Slide

  116. The END

    View Slide

  117. Questions?
    [email protected]
    @grmpyprogrammer

    View Slide