Slide 1

Slide 1 text

Become A Be!er, Grumpier Tester

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

But isn't TDD about tests?!?

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

Test-DRIVEN Development

Slide 6

Slide 6 text

You use tests to DRIVE the design of your application

Slide 7

Slide 7 text

The Grumpy Way Have an idea Write a test like everything works Write code until the test passes Repeat until you are internet- famous

Slide 8

Slide 8 text

No content

Slide 9

Slide 9 text

TDD As Design Tool We use the tests to help us build out a collection of loosely-coupled modules that accomplish well-defined tasks

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

The role of tests Verify the code is working correctly

Slide 12

Slide 12 text

The role of tests Prevent future regressions

Slide 13

Slide 13 text

The role of tests Document the code's behaviour

Slide 14

Slide 14 text

The role of tests Provide design guidance

Slide 15

Slide 15 text

The role of tests Support refactoring

Slide 16

Slide 16 text

Testing Building Blocks

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

TDD As Design Tool Code Kata #1 FizzBuzz

Slide 22

Slide 22 text

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'

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

TDD As Design Tool Code Kata #1 Arrange

Slide 25

Slide 25 text

TDD As Design Tool Code Kata #1 Act

Slide 26

Slide 26 text

TDD As Design Tool Code Kata #1 Assert

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

Testing Building Blocks Doubles

Slide 32

Slide 32 text

Testing Building Blocks (You probably call them mocks)

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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;

Slide 43

Slide 43 text

Testing Building Blocks So what double should I use?

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

Testing Building Blocks Unfortunately it depends

Slide 46

Slide 46 text

Testing Building Blocks Some guidelines

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

Code Kata #3 Catching spam

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

Code Kata #3 GO GO GO!

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

Code Coverage

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

Mutation Testing

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

Mutation Testing [fit] Infection

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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%

Slide 66

Slide 66 text

Difficult Language Constructs

Slide 67

Slide 67 text

Difficult Language Constructs Global objects

Slide 68

Slide 68 text

Difficult Language Constructs Global objects They lurk everywhere and anywhere

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

Difficult Language Constucts Global objects

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

Difficult Language Constructs Tightly coupled-dependencies

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

Current Grumpy Practices for be!er organized tests

Slide 84

Slide 84 text

Current Grumpy Practices Create Testing Helpers

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

Current Grumpy Practices public function createSentinelWithLoggedInUser() { $sentinel = m::mock(SentinelWrapper::class); $sentinel->shouldReceive('check')->andReturn(true); $sentinel->shouldReceive('getUser')->andReturn($this->createUser()); return $sentinel; }

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

Current Grumpy Practices Good coding practices result in good tests

Slide 89

Slide 89 text

Current Grumpy Practices Try and avoid the file system

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

Current Grumpy Practices In-memory databases

Slide 93

Slide 93 text

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;

Slide 94

Slide 94 text

Current Grumpy Practices Nice compromise when you need an actual database

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

Current Grumpy Practices Data Providers Their use encourages more generic tests

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

Test-A!er Skills

Slide 104

Slide 104 text

Test-A!er Skills Identifying Dependencies

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

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

Slide 107

Slide 107 text

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

Slide 108

Slide 108 text

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

Slide 109

Slide 109 text

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

Slide 110

Slide 110 text

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

Slide 111

Slide 111 text

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

Slide 112

Slide 112 text

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

Slide 113

Slide 113 text

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

Slide 114

Slide 114 text

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

Slide 115

Slide 115 text

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

Slide 116

Slide 116 text

The END

Slide 117

Slide 117 text

Questions? [email protected] @grmpyprogrammer