Slide 1

Slide 1 text

@maccath | Katy Ereira | #LonghornPHP Legacy Code. testing & safe refactoring

Slide 2

Slide 2 text

@maccath | Katy Ereira | #LonghornPHP 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

Slide 3

Slide 3 text

@maccath | Katy Ereira | #LonghornPHP What is refactoring? ● Updating existing code without changing the application logic ● Improving code structure ● Usually done in small, incremental steps

Slide 4

Slide 4 text

@maccath | Katy Ereira | #LonghornPHP Why would I refactor? ● Improve other developer’s understanding ● Increase the application’s performance ● Introduce new technology ● Accommodate changes

Slide 5

Slide 5 text

@maccath | Katy Ereira | #LonghornPHP 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

Slide 6

Slide 6 text

@maccath | Katy Ereira | #LonghornPHP Why would I unit test? ● Aid in the documentation of the system ● To prevent regressions when changing code ● To test integration with new technology ● For validation of product requirements

Slide 7

Slide 7 text

@maccath | Katy Ereira | #LonghornPHP Paradox: can’t refactor untested code, but can’t test the code until it’s refactored.

Slide 8

Slide 8 text

@maccath | Katy Ereira | #LonghornPHP A solution 1. Rewrite the entire codebase 2. … 3. Profit! 1. Rewrite the entire codebase 2. … 3. Profit!

Slide 9

Slide 9 text

@maccath | Katy Ereira | #LonghornPHP A better solution 1. Perform just enough refactoring to make the code testable 2. Test the code 3. Lather, rinse, repeat. 4. Profit!

Slide 10

Slide 10 text

@maccath | Katy Ereira | #LonghornPHP Assumptions: you’re developing with PHP and you have access to a terminal with Composer.

Slide 11

Slide 11 text

@maccath | Katy Ereira | #LonghornPHP “ 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/

Slide 12

Slide 12 text

@maccath | Katy Ereira | #ScotPHP19 I’m still on PHP version 5.6 / 7.0 / 7.1 ! PHPUnit Version PHP Version Requirement Supported Until 10 8.1 Future release 9 >= 7.3 February 2, 2024 8 >= 7.2 February 3, 2023 7 7.1, 7.2, 7.3 February 7, 2020 6 7.0, 7.1, 7.2 February 8, 2019 5 5.6, 7.0, 7.1 February 2, 2018

Slide 13

Slide 13 text

@maccath | Katy Ereira | #ScotPHP19 Install via Composer: PHPUnit - installation $ composer require --dev phpunit/phpunit ^|version| $ ./vendor/bin/phpunit --version PHPUnit x.y.z by Sebastian Bergmann and contributors.

Slide 14

Slide 14 text

@maccath | Katy Ereira | #ScotPHP19 Anatomy of a unit test tests/ExampleTest.php class ExampleTest extends PHPUnit\Framework\TestCase { public function testSomeFunction() { $result = someFunction(); // Should return true $this->assertTrue($result); } }

Slide 15

Slide 15 text

@maccath | Katy Ereira | #ScotPHP19 Run your tests: Running the tests $ ./vendor/bin/phpunit tests PHPUnit 7.5.8 by Sebastian Bergmann and contributors. . 1 / 1 (100%) Time: 64 ms, Memory: 4.00 MB OK (1 test, 1 assertion)

Slide 16

Slide 16 text

@maccath | Katy Ereira | #LonghornPHP Mocking ● Create test doubles ● Verify the behaviour of your code ● Isolate the system under test

Slide 17

Slide 17 text

@maccath | Katy Ereira | #LonghornPHP Mocking - Caution! DO: ● Mock dependencies, when needed. ● Be aware of tautological testing. DON’T: ● Mock everything. ● Mock the system under test.

Slide 18

Slide 18 text

@maccath | Katy Ereira | #LonghornPHP “ 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

Slide 19

Slide 19 text

@maccath | Katy Ereira | #LonghornPHP Install via Composer: $ composer require --dev mockery/mockery Mockery - Installation

Slide 20

Slide 20 text

@maccath | Katy Ereira | #LonghornPHP Mockery - Usage tests/ExampleTest.php class ExampleTest extends PHPUnit\Framework\TestCase { public function setUp(): void { $mockObject = Mockery::mock(Object::class); } }

Slide 21

Slide 21 text

@maccath | Katy Ereira | #LonghornPHP Mockery - Usage tests/ExampleTest.php class ExampleTest extends PHPUnit\Framework\TestCase { public function setUp(): void { $mockObject = Mockery::mock(Object::class); $mockObject->shouldReceive('method') // Expect that ‘method’ ->once() // should be called once ->andReturn(true); // and return true } }

Slide 22

Slide 22 text

@maccath | Katy Ereira | #LonghornPHP Mockery - Usage tests/ExampleTest.php class ExampleTest extends PHPUnit\Framework\TestCase { public function tearDown(): void { Mockery::close(); // Always close after running tests! } }

Slide 23

Slide 23 text

@maccath | Katy Ereira | #LonghornPHP Disclaimer: the following examples are deliberately simple.

Slide 24

Slide 24 text

@maccath | Katy Ereira | #LonghornPHP Tricky Test Scenario #1 Interfering Output

Slide 25

Slide 25 text

@maccath | Katy Ereira | #LonghornPHP

Slide 26

Slide 26 text

@maccath | Katy Ereira | #LonghornPHP hillsAreAlive.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

Slide 27

Slide 27 text

@maccath | Katy Ereira | #LonghornPHP A method that echoes tests/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’ } }

Slide 28

Slide 28 text

@maccath | Katy Ereira | #LonghornPHP 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

Slide 29

Slide 29 text

@maccath | Katy Ereira | #LonghornPHP ● 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

Slide 30

Slide 30 text

@maccath | Katy Ereira | #LonghornPHP tests/YodelTest.php public function testNewYodel() { ob_start(); // Start capturing output. yodel(); } A method that echoes

Slide 31

Slide 31 text

@maccath | Katy Ereira | #LonghornPHP tests/YodelTest.php public function testNewYodel() { ob_start(); yodel(); ob_end_clean(); // Finish capturing output. } A method that echoes

Slide 32

Slide 32 text

@maccath | Katy Ereira | #LonghornPHP tests/YodelTest.php public function testNewYodel() { ob_start(); yodel(); $output = ob_get_contents(); // Get the output. ob_end_clean(); } A method that echoes

Slide 33

Slide 33 text

@maccath | Katy Ereira | #LonghornPHP tests/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

Slide 34

Slide 34 text

@maccath | Katy Ereira | #LonghornPHP tests/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

Slide 35

Slide 35 text

@maccath | Katy Ereira | #LonghornPHP Tricky Test Scenario #2 Static method calls

Slide 36

Slide 36 text

@maccath | Katy Ereira | #LonghornPHP

Slide 37

Slide 37 text

@maccath | Katy Ereira | #LonghornPHP Vegeta.php function getPowerLevel() { $result = Goku::powerLevel(); // Goku’s power level is static. return $result > 9000 ? "It’s over 9000!" : "Suppressed."; } A static method call

Slide 38

Slide 38 text

@maccath | Katy Ereira | #LonghornPHP tests/PowerLevelTest.php class PowerLevelTest extends PHPUnit\Framework\TestCase { public function testUnder9000() { $result = getPowerLevel(); } public function testOver9000() { $result = getPowerLevel(); } } A static method call

Slide 39

Slide 39 text

@maccath | Katy Ereira | #LonghornPHP Goku.php class Goku { static function powerLevel() { return 5000; // Always returns 5000. } } A static method call

Slide 40

Slide 40 text

@maccath | Katy Ereira | #LonghornPHP tests/PowerLevelTest.php public function testUnder9000() { $result = getPowerLevel(); $this->assertEquals('Suppressed.', $result); // Success! } A static method call

Slide 41

Slide 41 text

@maccath | Katy Ereira | #LonghornPHP tests/PowerLevelTest.php public function testOver9000() { $result = getPowerLevel(); $this->assertEquals('It’s over 9000!', $result); // Failure. } A static method call

Slide 42

Slide 42 text

@maccath | Katy Ereira | #LonghornPHP 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.

Slide 43

Slide 43 text

@maccath | Katy Ereira | #LonghornPHP A static method call tests/PowerLevelTest.php public function testOver9000() { $superSaiyanGoku = Mockery::mock('alias:Goku'); // Mock Goku $result = getPowerLevel(); $this->assertEquals('It’s over 9000!', $result); // Failure. }

Slide 44

Slide 44 text

@maccath | Katy Ereira | #LonghornPHP A static method call tests/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. }

Slide 45

Slide 45 text

@maccath | Katy Ereira | #LonghornPHP A static method call tests/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! }

Slide 46

Slide 46 text

@maccath | Katy Ereira | #LonghornPHP

Slide 47

Slide 47 text

@maccath | Katy Ereira | #LonghornPHP A static method call tests/PowerLevelTest.php /** * @runTestsInSeparateProcesses * @preserveGlobalState disabled */ class PowerLevelTest extends PHPUnit\Framework\TestCase { // ... }

Slide 48

Slide 48 text

@maccath | Katy Ereira | #LonghornPHP Tricky Test Scenario #3 Hard-coded dependencies

Slide 49

Slide 49 text

@maccath | Katy Ereira | #LonghornPHP

Slide 50

Slide 50 text

@maccath | Katy Ereira | #LonghornPHP PessimistOrOptimist.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

Slide 51

Slide 51 text

@maccath | Katy Ereira | #LonghornPHP WeatherForecast.php class WeatherForecast { static function isSunny() { return (bool) rand(0, 1); // Returns random true/false. } } A hard-coded dependency

Slide 52

Slide 52 text

@maccath | Katy Ereira | #LonghornPHP tests/OutlookTest.php class OutlookTest extends PHPUnit\Framework\TestCase { public function testHalfFull() { $result = getOutlook(); } public function testHalfEmpty() { $result = getOutlook(); } } A hard-coded dependency

Slide 53

Slide 53 text

@maccath | Katy Ereira | #LonghornPHP tests/OutlookTest.php public function testHalfFull() { $result = getOutlook(); $this->assertEquals('The glass is half full.', $result); } A hard-coded dependency

Slide 54

Slide 54 text

@maccath | Katy Ereira | #LonghornPHP tests/OutlookTest.php public function testHalfEmpty() { $result = getOutlook(); $this->assertEquals('The glass is half empty.', $result); } A hard-coded dependency

Slide 55

Slide 55 text

@maccath | Katy Ereira | #LonghornPHP Testing: hard-coded dependencies ● 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.

Slide 56

Slide 56 text

@maccath | Katy Ereira | #LonghornPHP tests/OutlookTest.php public function testHalfFull() { $result = getOutlook(); $this->assertEquals('The glass is half full.', $result); } A hard-coded dependency

Slide 57

Slide 57 text

@maccath | Katy Ereira | #LonghornPHP tests/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

Slide 58

Slide 58 text

@maccath | Katy Ereira | #LonghornPHP tests/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

Slide 59

Slide 59 text

@maccath | Katy Ereira | #LonghornPHP tests/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

Slide 60

Slide 60 text

@maccath | Katy Ereira | #LonghornPHP tests/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

Slide 61

Slide 61 text

@maccath | Katy Ereira | #LonghornPHP tests/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

Slide 62

Slide 62 text

@maccath | Katy Ereira | #LonghornPHP tests/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

Slide 63

Slide 63 text

@maccath | Katy Ereira | #LonghornPHP tests/OutlookTest.php /** * @runTestsInSeparateProcesses * @preserveGlobalState disabled */ class OutlookTest extends PHPUnit\Framework\TestCase { // ... } A hard-coded dependency

Slide 64

Slide 64 text

@maccath | Katy Ereira | #LonghornPHP ● Refactor the code to use an injected dependency. ● Mock the dependency. ● Inject the mocked dependency. ● Remove @runTestsInSeparateProcesses annotation. Refactoring: hard-coded dependencies

Slide 65

Slide 65 text

@maccath | Katy Ereira | #LonghornPHP myLegacyCode.php function getOutlook() { $weather = new WeatherForecast(); $outlook = $weather->isSunny() ? "full" : "empty"; return "The glass is half $outlook."; } A hard-coded dependency

Slide 66

Slide 66 text

@maccath | Katy Ereira | #LonghornPHP 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

Slide 67

Slide 67 text

@maccath | Katy Ereira | #LonghornPHP 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

Slide 68

Slide 68 text

@maccath | Katy Ereira | #LonghornPHP 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

Slide 69

Slide 69 text

@maccath | Katy Ereira | #LonghornPHP 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

Slide 70

Slide 70 text

@maccath | Katy Ereira | #LonghornPHP 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

Slide 71

Slide 71 text

@maccath | Katy Ereira | #LonghornPHP tests/OutlookTest.php /** * @runTestsInSeparateProcesses */ class OutlookTest extends PHPUnit\Framework\TestCase { // ... } A hard-coded dependency

Slide 72

Slide 72 text

@maccath | Katy Ereira | #LonghornPHP tests/OutlookTest.php /** * @runTestsInSeparateProcesses */ class OutlookTest extends PHPUnit\Framework\TestCase { // ... } A hard-coded dependency

Slide 73

Slide 73 text

@maccath | Katy Ereira | #LonghornPHP Tricky Test Scenario #4 Ungraceful exits

Slide 74

Slide 74 text

@maccath | Katy Ereira | #LonghornPHP Tricky Test Scenario #4 (a) Redirects

Slide 75

Slide 75 text

@maccath | Katy Ereira | #LonghornPHP

Slide 76

Slide 76 text

@maccath | Katy Ereira | #LonghornPHP holyGrail.php function enterCaveOfCaerbannog(bool $killerRabbit = false) { if ($killerRabbit) { header('Location: http://www.montypython.com'); } return "Behold!"; } Redirections

Slide 77

Slide 77 text

@maccath | Katy Ereira | #LonghornPHP tests/CaveOfCaerbannogTest.php class EnterCaveOfCaerbannogTest extends PHPUnit\Framework\TestCase { public function testEnterSafely() { enterCaveOfCaerbannog(); } public function testRunAway() { enterCaveOfCaerbannog($killerRabbit = true); } } Redirections

Slide 78

Slide 78 text

@maccath | Katy Ereira | #LonghornPHP tests/CaveOfCaerbannogTest.php /** * @runTestsInSeparateProcesses */ class EnterCaveOfCaerbannogTest extends PHPUnit\Framework\TestCase { public function testEnterSafely() { … } public function testRunAway() { … } } Redirections

Slide 79

Slide 79 text

@maccath | Katy Ereira | #LonghornPHP tests/CaveOfCaerbannogTest.php public function testEnterSafely() { $result = enterCaveOfCaerbannog(); $this->assertEquals('Behold!', $result); // Success! } Redirections

Slide 80

Slide 80 text

@maccath | Katy Ereira | #LonghornPHP tests/CaveOfCaerbannogTest.php public function testRunAway() { $result = enterCaveOfCaerbannog($killerRabbit = true); $this->assertNotEquals('Behold!', $result); // Failure. } Redirections

Slide 81

Slide 81 text

@maccath | Katy Ereira | #LonghornPHP Refactoring: Redirections ● Return/continue/break after (potentially) redirecting. or ● Set redirection header as the very last step.

Slide 82

Slide 82 text

@maccath | Katy Ereira | #LonghornPHP holyGrail.php function enterCaveOfCaerbannog(bool $killerRabbit = false) { if ($killerRabbit) { header('Location: http://www.montypython.com'); return; // Go no further! } return "Behold!"; } Redirections

Slide 83

Slide 83 text

@maccath | Katy Ereira | #LonghornPHP holyGrail.php function enterCaveOfCaerbannog(bool $killerRabbit = false) { if (!$killerRabbit) { return "Behold!"; // Return early. } header('Location: http://www.montypython.com'); } Redirections

Slide 84

Slide 84 text

@maccath | Katy Ereira | #LonghornPHP tests/CaveOfCaerbannogTest.php public function testRunAway() { $result = enterCaveOfCaerbannog($killerRabbit = true); $this->assertEmpty($result); // Success! } Redirections

Slide 85

Slide 85 text

@maccath | Katy Ereira | #LonghornPHP Tricky Test Scenario #4 (b) Exiting

Slide 86

Slide 86 text

@maccath | Katy Ereira | #LonghornPHP

Slide 87

Slide 87 text

@maccath | Katy Ereira | #LonghornPHP

Slide 88

Slide 88 text

@maccath | Katy Ereira | #LonghornPHP LizTruss.php function lizTruss(bool $quitter = false) { if (!$quitter) { return 'I am a fighter, not a quitter!'; } exit; } Exiting

Slide 89

Slide 89 text

@maccath | Katy Ereira | #LonghornPHP tests/LizTrussTest.php class LizTest extends PHPUnit\Framework\TestCase { public function testNotAQuitter() { $result = lizTruss($quitter = false); } public function testIsAQuitter() { $result = lizTruss($quitter = true); } } Exiting

Slide 90

Slide 90 text

@maccath | Katy Ereira | #LonghornPHP tests/LizTrussTest.php public function testNotAQuitter() { $result = lizTruss($quitter = false); $this->assertEquals('I am a fighter, not a quitter!', $result); } Exiting

Slide 91

Slide 91 text

@maccath | Katy Ereira | #LonghornPHP tests/LizTrussTest.php public function testIsAQuitter() { $result = lizTruss($quitter = true); $this->assertEmpty($result); // We never get here. } Exiting

Slide 92

Slide 92 text

@maccath | Katy Ereira | #LonghornPHP Refactoring: Exiting ● Create a wrapper around the exit functionality. ● Inject the exit wrapper. ● Safely refactor to use the exit wrapper. ● Mock the exit wrapper.

Slide 93

Slide 93 text

@maccath | Katy Ereira | #LonghornPHP DowningStreet.php class DowningStreet { public function leave() { exit; // Leave the building. } } Exiting

Slide 94

Slide 94 text

@maccath | Katy Ereira | #LonghornPHP LizTruss.php function lizTruss(bool $quitter = false, DowningStreet $no10 = null) // Inject { if (!$quitter) { … } exit; } Exiting

Slide 95

Slide 95 text

@maccath | Katy Ereira | #LonghornPHP LizTruss.php function lizTruss(bool $quitter = false, DowningStreet $no10 = null) { $no10 = $no10 ?? new DowningStreet(); // Set default. if (!$quitter) { … } exit; } Exiting

Slide 96

Slide 96 text

@maccath | Katy Ereira | #LonghornPHP LizTruss.php function lizTruss(bool $quitter = false, DowningStreet $downingSt = null) { $no10 = $no10 ?? new DowningStreet(); if (!$quitter) { … } $no10->leave(); // Leave. } Exiting

Slide 97

Slide 97 text

@maccath | Katy Ereira | #LonghornPHP tests/LizTrussTest.php public function testIsAQuitter() { $no10 = Mockery::mock(DowningStreet::class); // Mock no10. $result = lizTruss($quitter = true); $this->assertEmpty($result); // Failure. } Exiting

Slide 98

Slide 98 text

@maccath | Katy Ereira | #LonghornPHP LizTruss.php public function testLizTruss() { $no10 = Mockery::mock(DowningStreet::class) ->shouldIgnoreMissing(); // Do nothing... $result = lizTruss($quitter = true); $this->assertEmpty($result); // Failure. } Exiting

Slide 99

Slide 99 text

@maccath | Katy Ereira | #LonghornPHP tests/LizTrussTest.php public function testLizTruss() { $no10 = Mockery::mock(DowningStreet::class) ->shouldIgnoreMissing(); $result = lizTruss($quitter = true, $no10); $this->assertEmpty($result); // Success! } Exiting

Slide 100

Slide 100 text

@maccath | Katy Ereira | #LonghornPHP tests/LizTrussTest.php public function testLizTruss() { $no10 = Mockery::mock(DowningStreet::class) ->shouldIgnoreMissing(); $result = lizTruss($quitter = true, $no10); $this->assertEmpty($result); // Success! } Exiting

Slide 101

Slide 101 text

@maccath | Katy Ereira | #LonghornPHP Tricky Test Scenario #5 Filesystem interaction

Slide 102

Slide 102 text

@maccath | Katy Ereira | #LonghornPHP

Slide 103

Slide 103 text

@maccath | Katy Ereira | #LonghornPHP HitCounter.php function countHit() { $count = file_get_contents(__DIR__ . '/hit_counter.txt'); file_put_contents(__DIR__ . '/hit_counter.txt', ++$count); } Updating a file

Slide 104

Slide 104 text

@maccath | Katy Ereira | #LonghornPHP tests/HitCounterTest.php class HitCounterTest extends PHPUnit\Framework\TestCase { public function testCountsHit() { countHit(); } } Updating a file

Slide 105

Slide 105 text

@maccath | Katy Ereira | #LonghornPHP tests/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

Slide 106

Slide 106 text

@maccath | Katy Ereira | #LonghornPHP Refactoring: Updating a file ● Inject the filename. ● Create a test double hit counter file. ● Reset the file contents after each test.

Slide 107

Slide 107 text

@maccath | Katy Ereira | #LonghornPHP HitCounter.php function countHit() { $count = file_get_contents(__DIR__ . '/hit_counter.txt'); file_put_contents(__DIR__ . '/hit_counter.txt', ++$count); } Updating a file

Slide 108

Slide 108 text

@maccath | Katy Ereira | #LonghornPHP HitCounter.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

Slide 109

Slide 109 text

@maccath | Katy Ereira | #LonghornPHP HitCounter.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

Slide 110

Slide 110 text

@maccath | Katy Ereira | #LonghornPHP 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

Slide 111

Slide 111 text

@maccath | Katy Ereira | #LonghornPHP tests/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

Slide 112

Slide 112 text

@maccath | Katy Ereira | #LonghornPHP tests/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

Slide 113

Slide 113 text

@maccath | Katy Ereira | #LonghornPHP tests/HitCounterTest.php public function tearDown(): void { $hitCounterFile = __DIR__ . '/test_hit_counter.txt'; file_put_contents($hitCounterFile, 0); // Reset counter. } Updating a file

Slide 114

Slide 114 text

@maccath | Katy Ereira | #LonghornPHP “ 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/

Slide 115

Slide 115 text

@maccath | Katy Ereira | #LonghornPHP tests/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

Slide 116

Slide 116 text

@maccath | Katy Ereira | #LonghornPHP tests/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

Slide 117

Slide 117 text

@maccath | Katy Ereira | #LonghornPHP tests/HitCounterTest.php public function tearDown(): void { $hitCounterFile = __DIR__ . '/test_hit_counter.txt'; file_put_contents($hitCounterFile, 0); // Reset counter. } Fake filesystem

Slide 118

Slide 118 text

@maccath | Katy Ereira | #LonghornPHP tests/HitCounterTest.php public function tearDown(): void { $hitCounterFile = __DIR__ . '/test_hit_counter.txt'; file_put_contents($hitCounterFile, 0); // Reset counter. } Fake filesystem

Slide 119

Slide 119 text

@maccath | Katy Ereira | #LonghornPHP Tricky Test Scenario #6 Spaghetti code

Slide 120

Slide 120 text

@maccath | Katy Ereira | #LonghornPHP

Slide 121

Slide 121 text

@maccath | Katy Ereira | #LonghornPHP Spaghetti code Bolognese.php class Bolognese { private $eaten; private $parmesan; public function __construct(bool $eaten = false) { … } public function eat() { … } public function isEaten(): bool { … } }

Slide 122

Slide 122 text

@maccath | Katy Ereira | #LonghornPHP Spaghetti code 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; }

Slide 123

Slide 123 text

@maccath | Katy Ereira | #LonghornPHP tests/BologneseTest.php class BologneseTest extends PHPUnit\Framework\TestCase { public function testEat() { $bolognese = new Bolognese(); $bolognese->eat(); } } Spaghetti code

Slide 124

Slide 124 text

@maccath | Katy Ereira | #LonghornPHP tests/BologneseTest.php public function testEat() { $bolognese = new Bolognese(); $bolognese->eat(); $this->assertTrue($bolognese->isEaten()); } Spaghetti code

Slide 125

Slide 125 text

@maccath | Katy Ereira | #LonghornPHP Testing: Spaghetti Code ● Use @covers annotation to isolate unit under test ● Create reusable expectations: ○ to illustrate different scenarios ○ to define alternate process branches

Slide 126

Slide 126 text

@maccath | Katy Ereira | #LonghornPHP tests/BologneseTest.php /** * @covers Bolognese */ public function testEat() { $bolognese = new Bolognese(); $bolognese->eat(); $this->assertTrue($bolognese->isEaten()); } Testing: Spaghetti Code

Slide 127

Slide 127 text

@maccath | Katy Ereira | #LonghornPHP tests/BologneseTest.php public function testEatDevouredBolognese() { … } public function testEatUneatenBolognese() { … } Testing: Spaghetti Code

Slide 128

Slide 128 text

@maccath | Katy Ereira | #LonghornPHP tests/BologneseTest.php private function eatenBolognese() { return new Bolognese($eaten = true); } private function uneatenBolognese() { return new Bolognese($eaten = false); } Testing: Spaghetti Code

Slide 129

Slide 129 text

@maccath | Katy Ereira | #LonghornPHP tests/BologneseTest.php private function expectGarlicBreadToBeEaten() { $garlicBread = Mockery::mock('overload:GarlicBread'); return $garlicBread->shouldReceive('eat'); // Return expectation. } Testing: Spaghetti Code

Slide 130

Slide 130 text

@maccath | Katy Ereira | #LonghornPHP tests/BologneseTest.php public function testEatDevouredBolognese() { $this->expectGarlicBreadToBeEaten()->never(); $this->eatenBolognese()->eat(); } public function testEatUneatenBolognese() { $this->expectGarlicBreadToBeEaten()->once(); $this->uneatenBolognese()->eat(); } Testing: Spaghetti Code

Slide 131

Slide 131 text

@maccath | Katy Ereira | #LonghornPHP Tricky Test Scenario #7 Private methods

Slide 132

Slide 132 text

@maccath | Katy Ereira | #LonghornPHP

Slide 133

Slide 133 text

@maccath | Katy Ereira | #LonghornPHP Private methods MCHammer.php class MCHammer { public function stop() { return $this->cantTouchThis(); } private function cantTouchThis() { return 'hammertime'; } }

Slide 134

Slide 134 text

@maccath | Katy Ereira | #LonghornPHP Private methods tests/MCHammerTest.php class MCHammerTest extends PHPUnit\Framework\TestCase { public function testCantTouchThis() { $mcHammer = new MCHammer(); $result = $mcHammer->cantTouchThis(); // Error; private method! $this->assertEquals('hammertime', $result); } }

Slide 135

Slide 135 text

@maccath | Katy Ereira | #LonghornPHP 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.

Slide 136

Slide 136 text

@maccath | Katy Ereira | #LonghornPHP Private methods tests/MCHammerTest.php class MCHammerTest extends PHPUnit\Framework\TestCase { public function testCantTouchThis() { $mcHammer = new MCHammer(); $result = $mcHammer->stop(); // Public method! $this->assertEquals('hammertime', $result); } }

Slide 137

Slide 137 text

@maccath | Katy Ereira | #LonghornPHP Private methods tests/MCHammerTest.php class MCHammerTest extends PHPUnit\Framework\TestCase { public function testCantTouchThis() { $mcHammer = new MCHammer(); $result = $mcHammer->stop(); $this->assertEquals('hammertime', $result); // Success! } }

Slide 138

Slide 138 text

@maccath | Katy Ereira | #LonghornPHP Time for a recap... ● Interfering output ● Static methods ● Hard-coded dependencies ● Ungraceful exits ● Filesystem interaction ● Spaghetti code ● Private methods

Slide 139

Slide 139 text

@maccath | Katy Ereira | #LonghornPHP https://joind.in/talk/e0655