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
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'
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
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; } }
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
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
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
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%
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'); } }
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); } }
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 *// } }
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" ); }
Current Grumpy Practices public function createSentinelWithLoggedInUser() { $sentinel = m::mock(SentinelWrapper::class); $sentinel->shouldReceive('check')->andReturn(true); $sentinel->shouldReceive('getUser')->andReturn($this->createUser()); return $sentinel; }
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'); }
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;
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.'], ]; }
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' ); }
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' ); }
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' );