After years of promoting PHPUnit I still hear it's hard to get started with unit testing. So instead of showing nice step-by-step examples on how to use PHPUnit, we're going to take an example straight from github. So I've taken the challenge to start writing tests for PHP projects that don't have unit tests in place and explain how I decide where to begin, how I approach my test strategy and how I ensure I’m covering each possible use-case (and covering the CRAP index). The goal of this presentation is to show everyone that even legacy code, spaghetti code and complex code bases can be tested. After this talk you can immediately apply my examples on your own codebase (even if it's a clean code base) and get started with testing. To follow along a basic knowledge unit testing with PHPUnit is required.
1. Your code
are my tests
How to test legacy code
in it2PROFESSIONAL PHP SERVICES
2. ADVISORY
IN ORDER TO EXPLAIN CERTAIN SITUATIONS YOU MIGHT FACE IN YOUR
DEVELOPMENT CAREER, WE WILL BE DISCUSSING THE USAGE OF
PRIVATES AND PUBLIC EXPOSURE. IF THESE TOPICS OFFEND OR
UPSET YOU, WE WOULD LIKE TO ASK YOU TO LEAVE THIS ROOM NOW.
THE SPEAKER NOR THE ORGANISATION CANNOT BE HELD
ACCOUNTABLE FOR MENTAL DISTRESS OR ANY FORMS OF DAMAGE
YOU MIGHT ENDURE DURING OR AFTER THIS PRESENTATION. FOR
COMPLAINTS PLEASE INFORM ORGANISATION AT INFO@IN2IT.BE.
3. Michelangelo van Dam
PHP Consultant
Community Leader
President of PHPBenelux
Contributor to PHP projects
T @DragonBe | F DragonBe
https://www.flickr.com/photos/akrabat/8784318813
4. Using Social Media?
Tag it #mytests
http://www.flickr.com/photos/andyofne/4633356197
http://www.flickr.com/photos/andyofne/4633356197
5. Why bother with testing?
https://www.flickr.com/photos/vialbost/5533266530
6. Most common excuses
why developers don’t test
• no time
• no budget
• deliver tests after finish project
(never)
• devs don’t know how
https://www.flickr.com/photos/dasprid/8147986307
10. Benefits of testing
• Direct feedback (test fails)
• Once a test is made, it will always be tested
• Easy to refactor existing code (protection)
• Easy to debug: write a test to see if a bug is
genuine
• Higher confidence and less uncertainty
11. Rule of thumb
“Whenever you are tempted to type something into a
print statement or a debugger expression, write it as
a test instead.”
— Source: Martin Fowler
13. PHPUnit
• PHPUnit is a port of xUnit testing framework
• Created by “Sebastian Bergmann”
• Uses “assertions” to verify behaviour of “unit of code”
• Open source and hosted on GitHub
• See https://github.com/sebastianbergmann/phpunit
• Can be installed using:
• PEAR
• PHAR
• Composer
14. Approach for testing
• Instantiate a “unit-of-code”
• Assert expected result against actual result
• Provide a custom error message
24. Testing for good
/** ... */
public function testClassAcceptsValidRequiredArgument()
{
$expected = $argument = 'Testing PHP Class';
$myClass = new MyClass;
$result = $myClass->doSomething($argument);
$this->assertSame($expected, $result,
'Expected result differs from actual result');
}
/** ... */
public function testClassAcceptsValidOptionalArgument()
{
$requiredArgument = 'Testing PHP Class';
$optionalArgument = 'Is this not fun?!?';
$expected = $requiredArgument . ' - ' . $optionalArgument;
$myClass = new MyClass;
$result = $myClass->doSomething($requiredArgument, $optionalArgument);
$this->assertSame($expected, $result,
'Expected result differs from actual result');
}
26. Example: testing payments
<?php
namespace
MyappCommonPayment;
class
ProcessTest
extends
PHPUnit_Framework_TestCase
{
public
function
testPaymentIsProcessedCorrectly()
{
$customer
=
new
Customer(/*
data
for
customer
*/);
$transaction
=
new
Transaction(/*
data
for
transaction
*/);
$process
=
new
Process('sale',
$customer,
$transaction);
$process-‐>pay();
$this-‐>assertTrue($process-‐>paymentApproved());
$this-‐>assertEquals('PAY-‐17S8410768582940NKEE66EQ',
$process-‐
>getPaymentId());
}
}
27. We don’t live in a fairy tale!
https://www.flickr.com/photos/bertknot/8175214909
42. Test for second condition
public function testLoadingNonExistingModuleIsNotExecuted()
{
$module = 'Foo_Bar';
$result = ModuleManager::include_install($module);
$this-
>assertFalse($result, 'Expecting failure for loading Foo_Bar');
$this->assertEmpty(ModuleManager::$modules_install,
'Expecting to find no modules ready for installation');
}
45. Test for third condition
public function testNoInstallationOfModuleWithoutInstallationClass(
)
{
$module = 'EssClient_IClient';
$result = ModuleManager::include_install($module);
$this-
>assertFalse($result, 'Expecting failure for loading Foo_Bar');
$this->assertEmpty(ModuleManager::$modules_install,
'Expecting to find no modules ready for installation');
}
60. Let’s use the static
$params = array (
'moduleName' => 'Foo_Bar',
'minVersion' => 0,
'maxVersion' => 1,
'maxOk' => true,
);
// We use a static method for this test
$dependency = Dependency::requires_range(
$params['moduleName'],
$params['minVersion'],
$params['maxVersion'],
$params['maxOk']
);
// We use reflection to see if properties are set correctly
$reflectionClass = new ReflectionClass('Dependency');
61. Use the reflection to assert
// Let's retrieve the private properties
$moduleName = $reflectionClass->getProperty('module_name');
$moduleName->setAccessible(true);
$minVersion = $reflectionClass->getProperty('version_min');
$minVersion->setAccessible(true);
$maxVersion = $reflectionClass->getProperty('version_max');
$maxVersion->setAccessible(true);
$maxOk = $reflectionClass->getProperty('compare_max');
$maxOk->setAccessible(true);
// Let's assert
$this->assertEquals($params['moduleName'], $moduleName->getValue($dependency),
'Expected value does not match the value set’);
$this->assertEquals($params['minVersion'], $minVersion->getValue($dependency),
'Expected value does not match the value set’);
$this->assertEquals($params['maxVersion'], $maxVersion->getValue($dependency),
'Expected value does not match the value set’);
$this->assertEquals('<=', $maxOk->getValue($dependency),
'Expected value does not match the value set');