Share
Embed
Legacy Code - Testing and Safe Refactoring
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!
Katy Ereira
February 21, 2020
Transcript
138 slides
-
@maccath | Katy Ereira | #PHPUK20 Legacy Code. testing &
safe refactoring -
@maccath | Katy Ereira | #PHPUK20 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 | #PHPUK20 What is refactoring? •
Updating existing code without changing the application logic • Improving code structure • Usually done in small, incremental steps -
@maccath | Katy Ereira | #PHPUK20 Why would I refactor?
• Improve other developer’s understanding • Increase the application’s performance • Introduce new technology • Accommodate changes -
@maccath | Katy Ereira | #PHPUK20 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 | #PHPUK20 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 -
@maccath | Katy Ereira | #PHPUK20 Paradox: can’t refactor untested
code, but can’t test the code until it’s refactored. -
@maccath | Katy Ereira | #PHPUK20 A solution 1. Rewrite
the entire codebase 2. … 3. Profit! 1. Rewrite the entire codebase 2. … 3. Profit! -
@maccath | Katy Ereira | #PHPUK20 A better solution 1.
Perform just enough refactoring to make the code testable 2. Test the code 3. Lather, rinse, repeat. 4. Profit! -
@maccath | Katy Ereira | #PHPUK20 Assumptions: you’re developing with
PHP and you have access to a terminal with Composer. -
@maccath | Katy Ereira | #PHPUK20 “ 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 | #ScotPHP19 I’m still on PHP
version 5.6 / 7.0 / 7.1 ! PHPUnit Version PHP Version Requirement Supported Until 9 7.3, 7.4 Future release 8 7.2, 7.3, 7.4 February 5, 2021 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 -
@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. -
@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); } } -
@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) -
@maccath | Katy Ereira | #PHPUK20 Mocking • Create test
doubles • Verify the behaviour of your code • Isolate the system under test -
@maccath | Katy Ereira | #PHPUK20 Mocking - Caution! DO:
• Mock dependencies, when needed. • Be aware of tautological testing. DON’T: • Mock everything. • Mock the system under test. -
@maccath | Katy Ereira | #PHPUK20 “ 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 | #PHPUK20 Install via Composer: $
composer require --dev mockery/mockery Mockery - Installation -
@maccath | Katy Ereira | #PHPUK20 Mockery - Usage tests/ExampleTest.php
class ExampleTest extends PHPUnit\Framework\TestCase { public function setUp(): void { $mockObject = Mockery::mock(Object::class); } } -
@maccath | Katy Ereira | #PHPUK20 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 } } -
@maccath | Katy Ereira | #PHPUK20 Mockery - Usage tests/ExampleTest.php
class ExampleTest extends PHPUnit\Framework\TestCase { public function tearDown(): void { Mockery::close(); // Always close after running tests! } } -
@maccath | Katy Ereira | #PHPUK20 Disclaimer: the following examples
are deliberately simple. -
@maccath | Katy Ereira | #PHPUK20 Tricky Test Scenario #1
Interfering Output -
@maccath | Katy Ereira | #PHPUK20 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 -
@maccath | Katy Ereira | #PHPUK20 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’ } } -
@maccath | Katy Ereira | #PHPUK20 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 | #PHPUK20 • 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 | #PHPUK20 tests/YodelTest.php public function testNewYodel()
{ ob_start(); // Start capturing output. yodel(); } A method that echoes -
@maccath | Katy Ereira | #PHPUK20 tests/YodelTest.php public function testNewYodel()
{ ob_start(); yodel(); ob_end_clean(); // Finish capturing output. } A method that echoes -
@maccath | Katy Ereira | #PHPUK20 tests/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 | #PHPUK20 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 -
@maccath | Katy Ereira | #PHPUK20 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 -
@maccath | Katy Ereira | #PHPUK20 Tricky Test Scenario #2
Static method calls -
@maccath | Katy Ereira | #PHPUK20 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 -
@maccath | Katy Ereira | #PHPUK20 tests/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 | #PHPUK20 Goku.php class Goku {
static function powerLevel() { return 5000; // Always returns 5000. } } A static method call -
@maccath | Katy Ereira | #PHPUK20 tests/PowerLevelTest.php public function testUnder9000()
{ $result = getPowerLevel(); $this->assertEquals('Suppressed.', $result); // Success! } A static method call -
@maccath | Katy Ereira | #PHPUK20 tests/PowerLevelTest.php public function testOver9000()
{ $result = getPowerLevel(); $this->assertEquals('It’s over 9000!', $result); // Failure. } A static method call -
@maccath | Katy Ereira | #PHPUK20 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 | #PHPUK20 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. } -
@maccath | Katy Ereira | #PHPUK20 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. } -
@maccath | Katy Ereira | #PHPUK20 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! } -
@maccath | Katy Ereira | #PHPUK20 A static method call
tests/PowerLevelTest.php /** * @runTestsInSeparateProcesses * @preserveGlobalState disabled */ class PowerLevelTest extends PHPUnit\Framework\TestCase { // ... } -
@maccath | Katy Ereira | #PHPUK20 Tricky Test Scenario #3
Hard-coded dependencies -
@maccath | Katy Ereira | #PHPUK20 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 -
@maccath | Katy Ereira | #PHPUK20 WeatherForecast.php class WeatherForecast {
static function isSunny() { return (bool) rand(0, 1); // Returns random true/false. } } A hard-coded dependency -
@maccath | Katy Ereira | #PHPUK20 tests/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 | #PHPUK20 tests/OutlookTest.php public function testHalfFull()
{ $result = getOutlook(); $this->assertEquals('The glass is half full.', $result); } A hard-coded dependency -
@maccath | Katy Ereira | #PHPUK20 tests/OutlookTest.php public function testHalfEmpty()
{ $result = getOutlook(); $this->assertEquals('The glass is half empty.', $result); } A hard-coded dependency -
@maccath | Katy Ereira | #PHPUK20 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. -
@maccath | Katy Ereira | #PHPUK20 tests/OutlookTest.php public function testHalfFull()
{ $result = getOutlook(); $this->assertEquals('The glass is half full.', $result); } A hard-coded dependency -
@maccath | Katy Ereira | #PHPUK20 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 -
@maccath | Katy Ereira | #PHPUK20 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 -
@maccath | Katy Ereira | #PHPUK20 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 -
@maccath | Katy Ereira | #PHPUK20 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 -
@maccath | Katy Ereira | #PHPUK20 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 -
@maccath | Katy Ereira | #PHPUK20 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 -
@maccath | Katy Ereira | #PHPUK20 tests/OutlookTest.php /** * @runTestsInSeparateProcesses
* @preserveGlobalState disabled */ class OutlookTest extends PHPUnit\Framework\TestCase { // ... } A hard-coded dependency -
@maccath | Katy Ereira | #PHPUK20 • 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 | #PHPUK20 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 | #PHPUK20 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 | #PHPUK20 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 | #PHPUK20 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 | #PHPUK20 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 | #PHPUK20 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 | #PHPUK20 tests/OutlookTest.php /** * @runTestsInSeparateProcesses
*/ class OutlookTest extends PHPUnit\Framework\TestCase { // ... } A hard-coded dependency -
@maccath | Katy Ereira | #PHPUK20 tests/OutlookTest.php /** * @runTestsInSeparateProcesses
*/ class OutlookTest extends PHPUnit\Framework\TestCase { // ... } A hard-coded dependency -
@maccath | Katy Ereira | #PHPUK20 Tricky Test Scenario #4
Ungraceful exits -
@maccath | Katy Ereira | #PHPUK20 holyGrail.php function enterCaveOfCaerbannog(bool $killerRabbit
= false) { if ($killerRabbit) { header('Location: http://www.montypython.com'); } return "Behold!"; } Redirections -
@maccath | Katy Ereira | #PHPUK20 tests/CaveOfCaerbannogTest.php class EnterCaveOfCaerbannogTest extends
PHPUnit\Framework\TestCase { public function testEnterSafely() { enterCaveOfCaerbannog(); } public function testRunAway() { enterCaveOfCaerbannog($killerRabbit = true); } } Redirections -
@maccath | Katy Ereira | #PHPUK20 tests/CaveOfCaerbannogTest.php /** * @runTestsInSeparateProcesses
*/ class EnterCaveOfCaerbannogTest extends PHPUnit\Framework\TestCase { public function testEnterSafely() { … } public function testRunAway() { … } } Redirections -
@maccath | Katy Ereira | #PHPUK20 tests/CaveOfCaerbannogTest.php public function testEnterSafely()
{ $result = enterCaveOfCaerbannog(); $this->assertEquals('Behold!', $result); // Success! } Redirections -
@maccath | Katy Ereira | #PHPUK20 tests/CaveOfCaerbannogTest.php public function testRunAway()
{ $result = enterCaveOfCaerbannog($killerRabbit = true); $this->assertNotEquals('Behold!', $result); // Failure. } Redirections -
@maccath | Katy Ereira | #PHPUK20 Refactoring: Redirections • Return/continue/break
after (potentially) redirecting. or • Set redirection header as the very last step. -
@maccath | Katy Ereira | #PHPUK20 holyGrail.php function enterCaveOfCaerbannog(bool $killerRabbit
= false) { if ($killerRabbit) { header('Location: http://www.montypython.com'); return; // Go no further! } return "Behold!"; } Redirections -
@maccath | Katy Ereira | #PHPUK20 holyGrail.php function enterCaveOfCaerbannog(bool $killerRabbit
= false) { if (!$killerRabbit) { return "Behold!"; // Return early. } header('Location: http://www.montypython.com'); } Redirections -
@maccath | Katy Ereira | #PHPUK20 tests/CaveOfCaerbannogTest.php public function testRunAway()
{ $result = enterCaveOfCaerbannog($killerRabbit = true); $this->assertEmpty($result); // Success! } Redirections -
@maccath | Katy Ereira | #PHPUK20 Brexit.php function brexit(bool $remainer
= false) { if ($remainer) { return 'Bollocks to Brexit!'; } exit; } Exiting -
@maccath | Katy Ereira | #PHPUK20 tests/BrexitTest.php class BrexitTest extends
PHPUnit\Framework\TestCase { public function testBrexit() { $result = brexit(); } public function testRemainer() { $result = brexit($remainer = true); } } Exiting -
@maccath | Katy Ereira | #PHPUK20 tests/BrexitTest.php public function testRemainer()
{ $result = brexit($remainer = true); $this->assertEquals('Bollocks to Brexit!', $result); // Success! } Exiting -
@maccath | Katy Ereira | #PHPUK20 tests/BrexitTest.php public function testBrexit()
{ $result = brexit(); $this->assertEmpty($result); // We never get here. } Exiting -
@maccath | Katy Ereira | #PHPUK20 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 | #PHPUK20 Boris.php class Boris {
public function getBrexitDone() { exit; // Completely exits. } } Exiting -
@maccath | Katy Ereira | #PHPUK20 Brexit.php function brexit(bool $remainer,
Boris $boris = null) // Inject { if ($remainer) { … } exit; } Exiting -
@maccath | Katy Ereira | #PHPUK20 Brexit.php function brexit(bool $remainer,
Boris $boris = null) { $boris = $boris ?? new Boris(); // Set default. if ($remainer) { … } exit; } Exiting -
@maccath | Katy Ereira | #PHPUK20 Brexit.php function brexit(bool $remainer,
Boris $boris = null) { $boris = $boris ?? new Boris(); if ($remainer) { … } $boris->getBrexitDone(); // Be Boris. } Exiting -
@maccath | Katy Ereira | #PHPUK20 tests/BrexitTest.php public function testBrexit()
{ $mockBoris = Mockery::mock(Boris::class); // Mock Boris. $result = brexit(); $this->assertEmpty($result); // Failure. } Exiting -
@maccath | Katy Ereira | #PHPUK20 tests/BrexitTest.php public function testBrexit()
{ $mockBoris = Mockery::mock(Boris::class) ->shouldIgnoreMissing(); // Do nothing... $result = brexit(); $this->assertEmpty($result); // Failure. } Exiting -
@maccath | Katy Ereira | #PHPUK20 tests/BrexitTest.php public function testBrexit()
{ $mockBoris = Mockery::mock(Boris::class) ->shouldIgnoreMissing(); $result = brexit($remainer = false, $mockBoris); // Use Boris. $this->assertEmpty($result); // Success! } Exiting -
@maccath | Katy Ereira | #PHPUK20 tests/BrexitTest.php public function testBrexit()
{ $mockBoris = Mockery::mock(Boris::class) ->shouldIgnoreMissing(); $result = brexit($remainer = false, $mockBoris); $this->assertEmpty($result); // Success! } Exiting -
@maccath | Katy Ereira | #PHPUK20 Tricky Test Scenario #5
Filesystem interaction -
@maccath | Katy Ereira | #PHPUK20 HitCounter.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 | #PHPUK20 tests/HitCounterTest.php class HitCounterTest extends
PHPUnit\Framework\TestCase { public function testCountsHit() { countHit(); } } Updating a file -
@maccath | Katy Ereira | #PHPUK20 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 -
@maccath | Katy Ereira | #PHPUK20 Refactoring: Updating a file
• Inject the filename. • Create a test double hit counter file. • Reset the file contents after each test. -
@maccath | Katy Ereira | #PHPUK20 HitCounter.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 | #PHPUK20 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 -
@maccath | Katy Ereira | #PHPUK20 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 -
@maccath | Katy Ereira | #PHPUK20 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 | #PHPUK20 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 -
@maccath | Katy Ereira | #PHPUK20 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 -
@maccath | Katy Ereira | #PHPUK20 tests/HitCounterTest.php public function tearDown():
void { $hitCounterFile = __DIR__ . '/test_hit_counter.txt'; file_put_contents($hitCounterFile, 0); // Reset counter. } Updating a file -
@maccath | Katy Ereira | #PHPUK20 “ 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 | #PHPUK20 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 -
@maccath | Katy Ereira | #PHPUK20 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 -
@maccath | Katy Ereira | #PHPUK20 tests/HitCounterTest.php public function tearDown():
void { $hitCounterFile = __DIR__ . '/test_hit_counter.txt'; file_put_contents($hitCounterFile, 0); // Reset counter. } Fake filesystem -
@maccath | Katy Ereira | #PHPUK20 tests/HitCounterTest.php public function tearDown():
void { $hitCounterFile = __DIR__ . '/test_hit_counter.txt'; file_put_contents($hitCounterFile, 0); // Reset counter. } Fake filesystem -
@maccath | Katy Ereira | #PHPUK20 Tricky Test Scenario #6
Spaghetti code -
@maccath | Katy Ereira | #PHPUK20 Spaghetti code Bolognese.php class
Bolognese { private $eaten; private $parmesan; public function __construct(bool $eaten = false) { … } public function eat() { … } public function isEaten(): bool { … } } -
@maccath | Katy Ereira | #PHPUK20 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; } -
@maccath | Katy Ereira | #PHPUK20 tests/BologneseTest.php class BologneseTest extends
PHPUnit\Framework\TestCase { public function testEat() { $bolognese = new Bolognese(); $bolognese->eat(); } } Spaghetti code -
@maccath | Katy Ereira | #PHPUK20 tests/BologneseTest.php public function testEat()
{ $bolognese = new Bolognese(); $bolognese->eat(); $this->assertTrue($bolognese->isEaten()); } Spaghetti code -
@maccath | Katy Ereira | #PHPUK20 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 | #PHPUK20 tests/BologneseTest.php /** * @covers
Bolognese */ public function testEat() { $bolognese = new Bolognese(); $bolognese->eat(); $this->assertTrue($bolognese->isEaten()); } Testing: Spaghetti Code -
@maccath | Katy Ereira | #PHPUK20 tests/BologneseTest.php public function testEatDevouredBolognese()
{ … } public function testEatUneatenBolognese() { … } Testing: Spaghetti Code -
@maccath | Katy Ereira | #PHPUK20 tests/BologneseTest.php private function eatenBolognese()
{ return new Bolognese($eaten = true); } private function uneatenBolognese() { return new Bolognese($eaten = false); } Testing: Spaghetti Code -
@maccath | Katy Ereira | #PHPUK20 tests/BologneseTest.php private function expectGarlicBreadToBeEaten()
{ $garlicBread = Mockery::mock('overload:GarlicBread'); return $garlicBread->shouldReceive('eat'); // Return expectation. } Testing: Spaghetti Code -
@maccath | Katy Ereira | #PHPUK20 tests/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 | #PHPUK20 Tricky Test Scenario #7
Private methods -
@maccath | Katy Ereira | #PHPUK20 Private methods MCHammer.php class
MCHammer { public function stop() { return $this->cantTouchThis(); } private function cantTouchThis() { return 'hammertime'; } } -
@maccath | Katy Ereira | #PHPUK20 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); } } -
@maccath | Katy Ereira | #PHPUK20 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 | #PHPUK20 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); } } -
@maccath | Katy Ereira | #PHPUK20 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! } } -
@maccath | Katy Ereira | #PHPUK20 Time for a recap...
• Interfering output • Static methods • Hard-coded dependencies • Ungraceful exits • Filesystem interaction • Spaghetti code • Private methods