Share
Embed
Legacy Code - Testing and Safe Refactoring - Laravel Live UK 2019
"I can't test this code because it's legacy. I need to update the code to make it testable. How can I manage that, without breaking existing functionality? I'll need to write some tests, but... argh!"
We've all been there! In this talk we'll look at safe ways to refactor 'untestable' code to make it testable - without breaking any existing functionality. Promise! Then, further changes will be much easier!
Katy Ereira
June 11, 2019
Transcript
135 slides
-
@maccath | Katy Ereira | #LaravelLive Legacy Code. testing &
safe refactoring -
@maccath | Katy Ereira | #LaravelLive What is legacy code?
• Code that wasn’t written by you, just now • Code resulting from incorrect assumptions • Code built using unsupported technology • Code that isn’t tested -
@maccath | Katy Ereira | #LaravelLive What is refactoring? •
Changing existing code without changing the application logic • Improving code structure • Usually done in small, incremental steps -
@maccath | Katy Ereira | #LaravelLive Why would I refactor?
• Improve other developer’s understanding • Increase the application’s performance • Introduce new technology • Accommodate changes in product requirements -
@maccath | Katy Ereira | #LaravelLive What is unit testing?
• Verification that each individual ‘unit’ of an application works as intended • Units are the component parts of your application • Generally, each behaviour is a unit; likely to be contained within a function • Unit testing is not a substitute for other types of testing -
@maccath | Katy Ereira | #LaravelLive Why would I unit
test? • Aid in the documentation of the system • To prevent regressions when adding/changing code • To test integration with new technology • For validation of product requirements -
@maccath | Katy Ereira | #LaravelLive Paradox: can’t refactor untested
code, but can’t test the code until it’s refactored. -
@maccath | Katy Ereira | #LaravelLive A solution 1. Rewrite
the entire codebase 2. … 3. Profit! 1. Rewrite the entire codebase 2. … 3. Profit! -
@maccath | Katy Ereira | #LaravelLive A better solution 1.
Perform just enough refactoring to make the code testable 2. Test the code 3. Refactor the code further 4. Profit! -
@maccath | Katy Ereira | #LaravelLive Assumptions: you’re developing with
PHP and you have access to a terminal with Composer. -
@maccath | Katy Ereira | #LaravelLive “ PHPUnit is a
programmer-oriented testing framework for PHP. It is an instance of the xUnit architecture for unit testing frameworks. PHPUnit Reference: https:/ /phpunit.de/ -
@maccath | Katy Ereira | #LaravelLive Create a test using
Artisan: Anatomy of a unit test $ php artisan make:test ExampleTest --unit -
@maccath | Katy Ereira | #LaravelLive Anatomy of a unit
test tests/Unit/ExampleTest.php namespace Tests\Unit; use Tests\TestCase; class ExampleTest extends TestCase { public function testSomeFunction() { $result = someFunction(); // Should return true $this->assertTrue($result); } } -
@maccath | Katy Ereira | #LaravelLive Run your tests: Running
the tests $ ./vendor/bin/phpunit tests/Unit PHPUnit <version> by Sebastian Bergmann and contributors. . 1 / 1 (100%) Time: 64 ms, Memory: 4.00 MB OK (1 test, 1 assertion) -
@maccath | Katy Ereira | #LaravelLive “ Mockery Mockery is
a simple yet flexible PHP mock object framework for use in unit testing with PHPUnit, PHPSpec or any other testing framework. Reference: https:/ /github.com/mockery/mockery -
@maccath | Katy Ereira | #LaravelLive Install via Composer: $
composer require --dev mockery/mockery Mockery - Installation -
@maccath | Katy Ereira | #LaravelLive Mockery - Usage tests/Unit/ExampleTest.php
class ExampleTest extends PHPUnit\Framework\TestCase { public function setUp() { $mockObject = Mockery::mock(Object::class); // Mock object. } } -
@maccath | Katy Ereira | #LaravelLive Mockery - Usage tests/Unit/ExampleTest.php
class ExampleTest extends PHPUnit\Framework\TestCase { public function setUp() { $mockObject = Mockery::mock(Object::class); $mockObject->shouldReceive('method') // Expectation ->once() // Method called once ->andReturn(true); // Method returns true } } tests/Unit/ExampleTest.php -
@maccath | Katy Ereira | #LaravelLive Mockery - Usage tests/Unit/ExampleTest.php
class ExampleTest extends PHPUnit\Framework\TestCase { public function tearDown() { Mockery::close(); // Always close after running tests! } } tests/Unit/ExampleTest.php -
@maccath | Katy Ereira | #LaravelLive Mockery - Usage tests/Unit/ExampleTest.php
use Illuminate\Foundation\Testing\TestCase as BaseTestCase; class ExampleTest extends BaseTestCase { } -
@maccath | Katy Ereira | #LaravelLive Mocking - Caution! DO:
• Mock dependencies, when needed. • Be aware of tautological testing. DON’T: • Mock everything. • Mock the system under test. -
@maccath | Katy Ereira | #LaravelLive Disclaimer: the following examples
are deliberately simple. -
@maccath | Katy Ereira | #LaravelLive Tricky Test Scenario #1
Output interference -
@maccath | Katy Ereira | #LaravelLive myLegacyCode.php function yodel($tune =
null) { if ($tune) { echo $tune; // The yodel is echoed back } else { echo 'Odl lay ee'; // A yodel begins } } A method that echoes -
@maccath | Katy Ereira | #LaravelLive A method that echoes
tests/Unit/YodelTest.php class PrintedOutputTest extends PHPUnit\Framework\TestCase { public function testNewYodel() { yodel(); // Should echo ‘Odl lay ee’ } public function testEchoedYodel() { yodel('Lay hee hoo'); // Should echo ‘Lay hee hoo’ } } -
@maccath | Katy Ereira | #LaravelLive Run your tests: A
method that echoes $ ./vendor/bin/phpunit tests PHPUnit 7.5.8 by Sebastian Bergmann and contributors. ROdl lay eeR 2 / 2 (100%)Lay hee hoo Time: 60 ms, Memory: 4.00 MB There were 2 risky tests -
@maccath | Katy Ereira | #LaravelLive • Prevent output being
echoed to the test runner • Capture the output that would otherwise be echoed • Perform assertions on the captured output A method that echoes -
@maccath | Katy Ereira | #LaravelLive tests/Unit/YodelTest.php public function testNewYodel()
{ ob_start(); // Start capturing output. yodel(); } A method that echoes -
@maccath | Katy Ereira | #LaravelLive tests/Unit/YodelTest.php public function testNewYodel()
{ ob_start(); yodel(); ob_end_clean(); // Finish capturing output. } A method that echoes -
@maccath | Katy Ereira | #LaravelLive tests/Unit/YodelTest.php public function testNewYodel()
{ ob_start(); yodel(); $output = ob_get_contents(); // Get the output. ob_end_clean(); } A method that echoes -
@maccath | Katy Ereira | #LaravelLive tests/Unit/YodelTest.php public function testNewYodel()
{ ob_start(); yodel(); $output = ob_get_contents(); ob_end_clean(); $this->assertEquals('Odl lay ee', $output); // Success! } A method that echoes -
@maccath | Katy Ereira | #LaravelLive tests/Unit/YodelTest.php public function testEchoedYodel()
{ ob_start(); yodel('Lay hee hoo'); $output = ob_get_contents(); ob_end_clean(); $this->assertEquals('Lay hee hoo', $output); // Success! } A method that echoes -
@maccath | Katy Ereira | #LaravelLive Tricky Test Scenario #2
Hard-coded dependencies -
@maccath | Katy Ereira | #LaravelLive myLegacyCode.php function getPowerLevel() {
$result = Goku::powerLevel(); // Goku’s power level is static. return $result > 9000 ? "It’s over 9000!" : "Suppressed."; } A static method call -
@maccath | Katy Ereira | #LaravelLive tests/Unit/PowerLevelTest.php class PowerLevelTest extends
PHPUnit\Framework\TestCase { public function testUnder9000() { $result = getPowerLevel(); } public function testOver9000() { $result = getPowerLevel(); } } A static method call -
@maccath | Katy Ereira | #LaravelLive Goku.php class Goku {
static function powerLevel() { return 5000; // Always returns 5000. } } A static method call -
@maccath | Katy Ereira | #LaravelLive tests/Unit/PowerLevelTest.php public function testUnder9000()
{ $result = getPowerLevel(); $this->assertEquals('Suppressed.', $result); // Success! } A static method call -
@maccath | Katy Ereira | #LaravelLive tests/Unit/PowerLevelTest.php public function testOver9000()
{ $result = getPowerLevel(); $this->assertEquals('It’s over 9000!', $result); // Failure. } A static method call -
@maccath | Katy Ereira | #LaravelLive A static method call
• Create a new ‘mock’ of Goku, using a Mockery alias. • Set the return value of Goku::powerLevel to be 9001. • Run tests in separate processes. -
@maccath | Katy Ereira | #LaravelLive A static method call
tests/Unit/PowerLevelTest.php public function testOver9000() { $superSaiyanGoku = Mockery::mock('alias:Goku'); // Mock Goku $result = getPowerLevel(); $this->assertEquals('It’s over 9000!', $result); // Failure. } -
@maccath | Katy Ereira | #LaravelLive A static method call
tests/Unit/PowerLevelTest.php public function testOver9000() { $superSaiyanGoku = Mockery::mock('alias:Goku') ->shouldReceive('powerLevel') // Set Goku’s power level ->andReturn(9001); // to over 9000. . $result = getPowerLevel(); $this->assertEquals('It’s over 9000!', $result); // Failure. } -
@maccath | Katy Ereira | #LaravelLive A static method call
tests/Unit/PowerLevelTest.php /** * @runTestsInSeparateProcesses */ class PowerLevelTest extends PHPUnit\Framework\TestCase { // ... } -
@maccath | Katy Ereira | #LaravelLive A static method call
tests/Unit/PowerLevelTest.php public function testOver9000() { // Goku is super saiyan! Mockery::mock('alias:Goku') ->shouldReceive('powerLevel') ->andReturn(9001); . $result = getPowerLevel(); $this->assertEquals('It’s over 9000!', $result); // Success! } -
@maccath | Katy Ereira | #LaravelLive “ A static method
call - via a Facade Unlike traditional static method calls, facades may be mocked. This provides a great advantage over traditional static methods and grants you the same testability you would have if you were using dependency injection. Reference: https:/ /laravel.com/docs/5.8/mocking#mocking-facades -
@maccath | Katy Ereira | #LaravelLive A static method call
- via a Facade tests/Unit/PowerLevelTest.php public function testOver9000() { $superSaiyanGoku = Mockery::mock('alias:Goku') ->shouldReceive('powerLevel') ->andReturn(9001); . $result = getPowerLevel(); $this->assertEquals('It’s over 9000!', $result); // Success! } -
@maccath | Katy Ereira | #LaravelLive A static method call
- via a Facade tests/Unit/PowerLevelTest.php public function testOver9000() { // Goku is super saiyan! Goku::shouldReceive('powerLevel') ->andReturn(9001); . $result = getPowerLevel(); $this->assertEquals('It’s over 9000!', $result); // Success! } -
@maccath | Katy Ereira | #LaravelLive A static method call
- via a Facade tests/Unit/PowerLevelTest.php /** * @runTestsInSeparateProcesses */ class PowerLevelTest extends TestCase { // ... } -
@maccath | Katy Ereira | #LaravelLive A static method call
- via a Facade tests/Unit/PowerLevelTest.php /** * @runTestsInSeparateProcesses */ class PowerLevelTest extends TestCase { // ... } -
@maccath | Katy Ereira | #LaravelLive myLegacyCode.php function getOutlook() {
$weather = new WeatherForecast(); // A hard-coded dependency. $outlook = $weather->isSunny() ? "full" : "empty"; return "The glass is half $outlook."; } A hard-coded dependency -
@maccath | Katy Ereira | #LaravelLive WeatherForecast.php class WeatherForecast {
static function isSunny() { return (bool) rand(0, 1); // Returns random true/false. } } A hard-coded dependency -
@maccath | Katy Ereira | #LaravelLive tests/Unit/OutlookTest.php class OutlookTest extends
PHPUnit\Framework\TestCase { public function testHalfFull() { $result = getOutlook(); } public function testHalfEmpty() { $result = getOutlook(); } } A hard-coded dependency -
@maccath | Katy Ereira | #LaravelLive tests/Unit/OutlookTest.php public function testHalfFull()
{ $result = getOutlook(); $this->assertEquals('The glass is half full.', $result); } A hard-coded dependency -
@maccath | Katy Ereira | #LaravelLive tests/Unit/OutlookTest.php public function testHalfEmpty()
{ $result = getOutlook(); $this->assertEquals('The glass is half empty.', $result); } A hard-coded dependency -
@maccath | Katy Ereira | #LaravelLive A hard-coded dependency •
Mock the WeatherForecast, using a Mockery override. • Set the return value of WeatherForecast::isSunny to be true or false. • Perform assertions based on mocked return values. • Run tests in separate processes. -
@maccath | Katy Ereira | #LaravelLive tests/Unit/OutlookTest.php public function testHalfFull()
{ $result = getOutlook(); $this->assertEquals('The glass is half full.', $result); } A hard-coded dependency -
@maccath | Katy Ereira | #LaravelLive tests/Unit/OutlookTest.php public function testHalfFull()
{ $mockForecast = Mockery::mock('overload:WeatherForecast'); // Overload $result = getOutlook(); $this->assertEquals('The glass is half full.', $result); } A hard-coded dependency -
@maccath | Katy Ereira | #LaravelLive tests/Unit/OutlookTest.php public function testHalfFull()
{ $mockForecast = Mockery::mock('overload:WeatherForecast') ->shouldReceive('isSunny'); // Set expectation $result = getOutlook(); $this->assertEquals('The glass is half full.', $result); // Success! } A hard-coded dependency -
@maccath | Katy Ereira | #LaravelLive tests/Unit/OutlookTest.php public function testHalfFull()
{ $mockForecast = Mockery::mock('overload:WeatherForecast') ->shouldReceive('isSunny') ->andReturn(true); // Force return value true $result = getOutlook(); $this->assertEquals('The glass is half full.', $result); // Success! } A hard-coded dependency -
@maccath | Katy Ereira | #LaravelLive tests/Unit/OutlookTest.php public function testHalfEmpty()
{ $mockForecast = Mockery::mock('overload:WeatherForecast') ->shouldReceive('isSunny') ->andReturn(false); // Force return value false $result = getOutlook(); $this->assertEquals('The glass is half empty.', $result); // Success! } A hard-coded dependency -
@maccath | Katy Ereira | #LaravelLive tests/Unit/OutlookTest.php /** * @runTestsInSeparateProcesses
*/ class OutlookTest extends PHPUnit\Framework\TestCase { // ... } A hard-coded dependency -
@maccath | Katy Ereira | #LaravelLive • Refactor the code
to use an injected dependency. • Mock the dependency. • Inject the mocked dependency. • Remove @runTestsInSeparateProcesses annotation. Refactoring: hard-coded dependencies -
@maccath | Katy Ereira | #LaravelLive myLegacyCode.php function getOutlook() {
$weather = new WeatherForecast(); $outlook = $weather->isSunny() ? "full" : "empty"; return "The glass is half $outlook."; } A hard-coded dependency -
@maccath | Katy Ereira | #LaravelLive myLegacyCode.php function getOutlook(WeatherForecast $weather
= null) // Inject { $weather = new WeatherForecast(); $outlook = $weather->isSunny() ? "full" : "empty"; return "The glass is half $outlook."; } A hard-coded dependency -
@maccath | Katy Ereira | #LaravelLive myLegacyCode.php function getOutlook(WeatherForecast $weather
= null) { $weather = $weather ?? new WeatherForecast(); // Use injected dependency $outlook = $weather->isSunny() ? "full" : "empty"; return "The glass is half $outlook."; } A hard-coded dependency -
@maccath | Katy Ereira | #LaravelLive tests\OutlookTest.php public function testHalfFull()
{ $mockForecast = Mockery::mock('overload:WeatherForecast') ->shouldReceive('isSunny') ->andReturn(true); $result = getOutlook(); $this->assertEquals('The glass is half full.', $result); // Success! } A hard-coded dependency -
@maccath | Katy Ereira | #LaravelLive tests\OutlookTest.php public function testHalfFull()
{ $mockForecast = Mockery::mock(WeatherForecast::class) // No overload! ->shouldReceive('isSunny') ->andReturn(true); $result = getOutlook(); $this->assertEquals('The glass is half full.', $result); // Success! } A hard-coded dependency -
@maccath | Katy Ereira | #LaravelLive tests\OutlookTest.php public function testHalfFull()
{ $mockForecast = Mockery::mock(WeatherForecast::class) ->shouldReceive('isSunny') ->andReturn(true); $result = getOutlook($mockForecast); // Inject! $this->assertEquals('The glass is half full.', $result); // Success! } A hard-coded dependency -
@maccath | Katy Ereira | #LaravelLive tests/Unit/OutlookTest.php /** * @runTestsInSeparateProcesses
*/ class OutlookTest extends PHPUnit\Framework\TestCase { // ... } A hard-coded dependency -
@maccath | Katy Ereira | #LaravelLive tests/Unit/OutlookTest.php /** * @runTestsInSeparateProcesses
*/ class OutlookTest extends PHPUnit\Framework\TestCase { // ... } A hard-coded dependency -
@maccath | Katy Ereira | #LaravelLive Tricky Test Scenario #3
Ungraceful exits -
@maccath | Katy Ereira | #LaravelLive myLegacyCode.php function getMeOutOfHere(bool $imACelebrity
= false) { if ($imACelebrity) { header('Location: http://google.com'); } return "Nope!"; } Redirections -
@maccath | Katy Ereira | #LaravelLive tests/Unit/GetMeOutTest.php class GetMeOutTest extends
PHPUnit\Framework\TestCase { public function testLeaveMeHere() { getMeOutOfHere(); } public function testGetMeOut() { getMeOutOfHere($imACelebrity = true); } } Redirections -
@maccath | Katy Ereira | #LaravelLive tests/Unit/GetMeOutTest.php /** * @runTestsInSeparateProcesses
*/ class GetMeOutTest extends PHPUnit\Framework\TestCase { public function testLeaveMeHere() { … } public function testGetMeOut() { … } } Redirections -
@maccath | Katy Ereira | #LaravelLive tests/Unit/GetMeOutTest.php public function testLeaveMeHere()
{ $result = getMeOutOfHere(); $this->assertEquals('Nope!', $result); // Success! } Redirections -
@maccath | Katy Ereira | #LaravelLive tests/Unit/GetMeOutTest.php public function testGetMeOut()
{ $result = getMeOutOfHere($imACelebrity = true); $this->assertNotEquals('Nope!', $result); // Failure. } Redirections -
@maccath | Katy Ereira | #LaravelLive Refactoring: Redirections • Return/continue/break
after (potentially) redirecting. or • Set redirection header as the very last step. -
@maccath | Katy Ereira | #LaravelLive myLegacyCode.php function getMeOutOfHere(bool $imACelebrity
= false) { if ($imACelebrity) { header('Location: http://google.com'); return; // Don’t go any further! } return "Nope!"; } Redirections -
@maccath | Katy Ereira | #LaravelLive myLegacyCode.php function getMeOutOfHere(bool $imACelebrity
= false) { if (!$imACelebrity) { return "Nope!"; // Return early. } header('Location: http://google.com'); } Redirections -
@maccath | Katy Ereira | #LaravelLive tests/Unit/GetMeOutTest.php public function testGetMeOut()
{ $result = getMeOutOfHere($imACelebrity = true); $this->assertNotEquals('Nope!', $result); // Success! } Redirections -
@maccath | Katy Ereira | #LaravelLive myLegacyCode.php function brexit(bool $remoaner
= false) { if ($remoaner) { return 'People’s vote!'; } exit; } Exiting -
@maccath | Katy Ereira | #LaravelLive tests/Unit/BrexitTest.php class BrexitTest extends
PHPUnit\Framework\TestCase { public function testBrexit() { $result = brexit(); } public function testRemoaner() { $result = brexit($remoaner = true); } } Exiting -
@maccath | Katy Ereira | #LaravelLive tests/Unit/BrexitTest.php public function testRemoaner()
{ $result = brexit($remoaner = true); $this->assertEquals('People’s vote!', $result); // Success! } Exiting -
@maccath | Katy Ereira | #LaravelLive tests/Unit/BrexitTest.php public function testBrexit()
{ $result = brexit(); $this->assertEmpty($result); // Never gets here. } Exiting -
@maccath | Katy Ereira | #LaravelLive Refactoring: Exiting • Create
a wrapper around the exit functionality. • Inject the exit wrapper. • Safely refactor to use the exit wrapper. • Mock the exit wrapper. -
@maccath | Katy Ereira | #LaravelLive BrexitUtility.php class BrexitUtility {
public function brexitMeansBrexit() { exit; // Completely exits. } } Exiting -
@maccath | Katy Ereira | #LaravelLive myLegacyCode.php function brexit(bool $remoaner,
BrexitUtility $brexitUtil = null) // Inject { if ($remoaner) { … } exit; } Exiting -
@maccath | Katy Ereira | #LaravelLive myLegacyCode.php function brexit(bool $remoaner,
BrexitUtility $brexitUtil = null) { $brexitUtil = $brexitUtil ?? new BrexitUtility(); // Set default. if ($remoaner) { … } exit; } Exiting -
@maccath | Katy Ereira | #LaravelLive myLegacyCode.php function brexit(bool $remoaner,
BrexitUtility $brexitUtil = null) { $brexitUtil = $brexitUtil ?? new BrexitUtility(); if ($remoaner) { … } $brexitUtil->brexitMeansBrexit(); // Use the brexit utility. } Exiting -
@maccath | Katy Ereira | #LaravelLive tests/Unit/BrexitTest.php public function testBrexit()
{ $mockBrexitUtil = Mockery::mock(BrexitUtility::class); // Mock Brexit util $result = brexit(); $this->assertNotEquals('People’s vote!', $result); // Failure. } Exiting -
@maccath | Katy Ereira | #LaravelLive tests/Unit/BrexitTest.php public function testBrexit()
{ $mockBrexitUtil = Mockery::mock(BrexitUtility::class) ->shouldIgnoreMissing(); // Do nothing... $result = brexit(); $this->assertNotEquals('People’s vote!', $result); // Failure. } Exiting -
@maccath | Katy Ereira | #LaravelLive tests/Unit/BrexitTest.php public function testBrexit()
{ $mockBrexitUtil = Mockery::mock(BrexitUtility::class) ->shouldIgnoreMissing(); $result = brexit($remoaner = false, $mockBrexitUtil); // Use it. $this->assertNotEquals('People’s vote!', $result); // Success! } Exiting -
@maccath | Katy Ereira | #LaravelLive tests/Unit/BrexitTest.php public function testBrexit()
{ $mockBrexitUtil = Mockery::mock(BrexitUtility::class) ->shouldIgnoreMissing(); $result = brexit($remoaner = false, $mockBrexitUtil); $this->assertNotEquals('People’s vote!', $result); // Success! } Exiting -
@maccath | Katy Ereira | #LaravelLive myLegacyCode.php function countHit() {
$count = file_get_contents(__DIR__ . '/hit_counter.txt'); file_put_contents(__DIR__ . '/hit_counter.txt', ++$count); } Updating a file -
@maccath | Katy Ereira | #LaravelLive tests/Unit/HitCounterTest.php class HitCounterTest extends
PHPUnit\Framework\TestCase { public function testCountsHit() { countHit(); } } Updating a file -
@maccath | Katy Ereira | #LaravelLive tests/Unit/HitCounterTest.php public function testCountsHit()
{ $hitCounterFile = __DIR__ . '/../hit_counter.txt'; $countBefore = file_get_contents($hitCounterFile); countHit(); $countAfter = file_get_contents($hitCounterFile); $this->assertEquals($countBefore++, $countAfter); // Success! } Updating a file -
@maccath | Katy Ereira | #LaravelLive Refactoring: Updating a file
• Inject the filename. • Create a test hit counter file. • Reset the file contents after each test. -
@maccath | Katy Ereira | #LaravelLive myLegacyCode.php function countHit() {
$count = file_get_contents(__DIR__ . '/hit_counter.txt'); file_put_contents(__DIR__ . '/hit_counter.txt', ++$count); } Updating a file -
@maccath | Katy Ereira | #LaravelLive myLegacyCode.php function countHit($file =
__DIR__ . '/hit_counter.txt') // Inject $file { $count = file_get_contents(__DIR__ . '/hit_counter.txt'); file_put_contents(__DIR__ . '/hit_counter.txt', ++$count); } Updating a file -
@maccath | Katy Ereira | #LaravelLive myLegacyCode.php function countHit($file =
__DIR__ . '/hit_counter.txt') // Inject $file { $count = file_get_contents($file); // Use $file file_put_contents($file, ++$count); // Use $file } Updating a file -
@maccath | Katy Ereira | #LaravelLive tests/Unit/HitCounterTest.php public function testCountsHit()
{ $hitCounterFile = __DIR__ . '/hit_counter.txt'; $countBefore = file_get_contents($hitCounterFile); countHit(); $countAfter = file_get_contents($hitCounterFile); $this->assertEquals($countBefore++, $countAfter); } Updating a file -
@maccath | Katy Ereira | #LaravelLive tests/Unit/HitCounterTest.php public function testCountsHit()
{ $hitCounterFile = __DIR__ . '/test_hit_counter.txt'; // Test file! $countBefore = file_get_contents($hitCounterFile); countHit(); $countAfter = file_get_contents($hitCounterFile); $this->assertEquals($countBefore++, $countAfter); } Updating a file -
@maccath | Katy Ereira | #LaravelLive tests/Unit/HitCounterTest.php public function testCountsHit()
{ $hitCounterFile = __DIR__ . '/test_hit_counter.txt'; $countBefore = file_get_contents($hitCounterFile); countHit($hitCounterFile); // Use test hit counter file. $countAfter = file_get_contents($hitCounterFile); $this->assertEquals($countBefore++, $countAfter); } Updating a file -
@maccath | Katy Ereira | #LaravelLive tests/Unit/HitCounterTest.php public function tearDown()
{ $hitCounterFile = __DIR__ . '/test_hit_counter.txt'; file_put_contents($hitCounterFile, 0); // Reset counter. } Updating a file -
@maccath | Katy Ereira | #LaravelLive “ vfsStream is a
stream wrapper for a virtual file system that may be helpful in unit tests to mock the real file system. It can be used with any unit test framework, like PHPUnit or SimpleTest. Fake filesystem Reference: http:/ /vfs.bovigo.org/ -
@maccath | Katy Ereira | #LaravelLive tests/Unit/HitCounterTest.php public function testCountsHit()
{ $hitCounterFile = __DIR__ . '/test_hit_counter.txt'; $countBefore = file_get_contents($hitCounterFile); countHit($hitCounterFile); $countAfter = file_get_contents($hitCounterFile); $this->assertEquals($countBefore++, $countAfter); } Fake filesystem -
@maccath | Katy Ereira | #LaravelLive tests/Unit/HitCounterTest.php public function testCountsHit()
{ $vfs = vfsStream::setup(); $hitCounterFile = vfsStream::newFile('test_hit_counter.txt') ->at($vfs) ->setContent('0'); countHit($hitCounterFile->url()); $this->assertEquals(1, $hitCounterFile->getContent()); } Fake filesystem -
@maccath | Katy Ereira | #LaravelLive tests/Unit/HitCounterTest.php public function tearDown()
{ $hitCounterFile = __DIR__ . '/test_hit_counter.txt'; file_put_contents($hitCounterFile, 0); // Reset counter. } Fake filesystem -
@maccath | Katy Ereira | #LaravelLive tests/Unit/HitCounterTest.php public function tearDown()
{ $hitCounterFile = __DIR__ . '/test_hit_counter.txt'; file_put_contents($hitCounterFile, 0); // Reset counter. } Fake filesystem -
@maccath | Katy Ereira | #LaravelLive Spaghetti code src\Bolognese.php class
Bolognese { private $eaten; private $parmesan; public function __construct(bool $eaten = false) { … } public function eat() { … } public function isEaten(): bool { … } } -
@maccath | Katy Ereira | #LaravelLive Spaghetti code src\Bolognese.php public
function eat() { if ($this->eaten) return; // Can’t eat twice! if (!$this->parmesan || !$this->parmesan->isEaten()) { $this->parmesan = new Parmesan(); // Must always have parmesan. } (new GarlicBread())->eat(); // Gotta have garlic bread. $this->parmesan->eat(); $this->eaten = true; } -
@maccath | Katy Ereira | #LaravelLive tests/Unit/BologneseTest.php class BologneseTest extends
PHPUnit\Framework\TestCase { public function testEat() { $bolognese = new Bolognese(); $bolognese->eat(); } } Spaghetti code -
@maccath | Katy Ereira | #LaravelLive tests/Unit/BologneseTest.php public function testEat()
{ $bolognese = new Bolognese(); $bolognese->eat(); $this->assertTrue($bolognese->isEaten()); } Spaghetti code -
@maccath | Katy Ereira | #LaravelLive Testing: Spaghetti Code •
Use @covers annotation to isolate unit under test • Create reusable expectations: ◦ to illustrate different scenarios ◦ to define alternate process branches -
@maccath | Katy Ereira | #LaravelLive tests/Unit/BologneseTest.php /** * @covers
Bolognese */ public function testEat() { $bolognese = new Bolognese(); $bolognese->eat(); $this->assertTrue($bolognese->isEaten()); } Testing: Spaghetti Code -
@maccath | Katy Ereira | #LaravelLive tests/Unit/BologneseTest.php public function testEatDevouredBolognese()
{ … } public function testEatUneatenBolognese() { … } Testing: Spaghetti Code -
@maccath | Katy Ereira | #LaravelLive tests/Unit/BologneseTest.php private function eatenBolognese()
{ return new Bolognese($eaten = true); } private function uneatenBolognese() { return new Bolognese($eaten = false); } Testing: Spaghetti Code -
@maccath | Katy Ereira | #LaravelLive tests/Unit/BologneseTest.php private function expectGarlicBreadToBeEaten()
{ $garlicBread = Mockery::mock('overload:GarlicBread'); return $garlicBread->shouldReceive('eat'); // Return expectation. } Testing: Spaghetti Code -
@maccath | Katy Ereira | #LaravelLive tests/Unit/BologneseTest.php public function testEatDevouredBolognese()
{ $this->expectGarlicBreadToBeEaten()->never(); $this->eatenBolognese()->eat(); } public function testEatUneatenBolognese() { $this->expectGarlicBreadToBeEaten()->once(); $this->uneatenBolognese()->eat(); } Testing: Spaghetti Code -
@maccath | Katy Ereira | #LaravelLive Tricky Test Scenario Group
#6 Private methods -
@maccath | Katy Ereira | #LaravelLive Private methods MCHammer.php class
MCHammer { public function stop() { return $this->cantTouchThis(); } private function cantTouchThis() { return 'hammertime'; } } -
@maccath | Katy Ereira | #LaravelLive Private methods tests/Unit/MCHammerTest.php class
MCHammerTest extends PHPUnit\Framework\TestCase { public function testCantTouchThis() { $mcHammer = new MCHammer(); $result = $mcHammer->cantTouchThis(); // Error; private method! $this->assertEquals('hammertime', $result); } } -
@maccath | Katy Ereira | #LaravelLive Private methods • Don’t
test private methods. • Test the public methods that use private methods. • Private methods represent implementation details. • Private methods should remain invisible. -
@maccath | Katy Ereira | #LaravelLive Private methods tests/Unit/MCHammerTest.php class
MCHammerTest extends PHPUnit\Framework\TestCase { public function testCantTouchThis() { $mcHammer = new MCHammer(); $result = $mcHammer->stop(); // Public method! $this->assertEquals('hammertime', $result); } } -
@maccath | Katy Ereira | #LaravelLive Private methods tests/Unit/MCHammerTest.php class
MCHammerTest extends PHPUnit\Framework\TestCase { public function testCantTouchThis() { $mcHammer = new MCHammer(); $result = $mcHammer->stop(); $this->assertEquals('hammertime', $result); // Success! } } -
@maccath | Katy Ereira | #LaravelLive Thank you! Feedback welcome!
https:/ /joind.in/talk/718c8