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

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. 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
  2. The Grumpy Way Have an idea Write a test like

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

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

    https://www.devmynd.com/blog/five-factor-testing/
  5. 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
  6. 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'
  7. TDD As Design Tool Code Kata #2 Build a Magic

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

    array Sort the items by number of cards and then alphabetical
  9. 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
  10. 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
  11. Testing Building Blocks They are used when you need a

    dependency for a test and don't want to use the real thing
  12. 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; } }
  13. 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
  14. 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
  15. 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
  16. 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;
  17. Testing Building Blocks Some guidelines Always use the real thing

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

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

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

    to date Prophecy is currently the best choice
  21. 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
  22. 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
  23. Mutation Testing — Randomly changes your code ("mutating") — Looking

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

    very-specific testing scenarios to have a "high score"
  25. 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%
  26. Difficult Language Constructs Global objects Good practice - refactor to

    configuration objects Allows over-riding them in your arrange steps
  27. 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'); } }
  28. 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); } }
  29. 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 *// } }
  30. Diffuclt Language Constructs Service Locators Good practice seems to be

    think hard about what you're doing and minimize their use
  31. 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);
  32. 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);
  33. 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" ); }
  34. Current Grumpy Practices public function createSentinelWithLoggedInUser() { $sentinel = m::mock(SentinelWrapper::class);

    $sentinel->shouldReceive('check')->andReturn(true); $sentinel->shouldReceive('getUser')->andReturn($this->createUser()); return $sentinel; }
  35. 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'); }
  36. 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;
  37. 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]); }
  38. 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.'], ]; }
  39. 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()); }
  40. 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'], ]; }
  41. Current Grumpy Practices Data Providers Extend the provider to include

    more data instead of slightly-different tests
  42. 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')); }
  43. Test-A!er Skills Testing a non-authorized user trying to view a

    talk? if (!$this->service('sentinel')->check()) { return $this->redirectTo('login'); }
  44. 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' ); }
  45. 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'));
  46. 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' ); }
  47. 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' );
  48. Test-A!er Skills Sometimes the code needs to change to be

    more testable Consider making your application "environment-aware"