View
350
Download
2
Category
Preview:
Citation preview
THERE'S LIFE OUTSIDE OF TDDHofmeister, ideals are a beautiful thing, but
over the hills is too far away.
Loose translation from "The loony tales" by Kabaret Potem
WHAT?Tests that check if a certain unit of code (whatever it means)
works properly
Tests for given unit are independent from the rest of thecodebase and other tests
FOR WHAT?Easy regression findingCan make you more inclined to use certain good practices(dependency injection FTW)Helps catching dead codeDocumentation without documenting
WITH WHAT?PhpUnit!
There should be an installation manual
But let's be serious - nobody will remember that
A UNIT OF CODEclass LoremIpsum{ private $dependency;
public function getDependency() { return $this->dependency; }
public function setDependency(Dependency $dependency) { $this->dependency = $dependency; }}
A UNIT OF CODE CONTINUEDclass LoremIpsum{ public function doStuff() { if (!$this->dependency) { throw new \Exception("I really need this, mate"); } $result = array(); foreach ($this->dependency->getResults() as $value) { $value = 42 / $value; $value .= ' suffix'; $result[$value] = true; } return $result; }}
WE DON'T WANT TO TEST THOSEclass LoremIpsum{ public function getDependency() { return $this->dependency; }
public function setDependency(Dependency $dependency) { $this->dependency = $dependency; }}
WHAT DO WE NEED TO TEST HERE?public function doStuff(){ if (!$this->dependency) { throw new \Exception("I really need this, mate"); }
$result = array();
foreach ($this->dependency->getResults() as $value) { $value = 42 / $value; $value .= ' suffix'; $result[$value] = true; }
return $result;}
WHAT DO WE NEED TO TEST HERE?public function doStuff(){ if (!$this->dependency) { throw new \Exception("I really need this, mate"); }
$result = array();
foreach ($this->dependency->getResults() as $value) { $value = 42 / $value; $value .= ' suffix'; $result[$value] = true; }
return $result;}
WHAT DO WE NEED TO TEST HERE?public function doStuff(){ if (!$this->dependency) { throw new \Exception("I really need this, mate"); }
$result = array();
foreach ($this->dependency->getResults() as $value) { $value = 42 / $value; $value .= ' suffix'; $result[$value] = true; }
return $result;}
WHY NOT THIS?public function doStuff(){ if (!$this->dependency) { throw new \Exception("I really need this, mate"); }
$result = array();
foreach ($this->dependency->getResults() as $value) { $value = 42 / $value; $value .= ' suffix'; $result[$value] = true; }
return $result;}
THIS TOO, BUT...1. Unit testing doesn't replace debugging.2. Unit testing can make your code better, but won't really do
anything for you.3. Unit testing focus your attention on what the code does, so
you can spot potential problems easier.
LET'S CHECK IF IT WORKSclass LoremIpsumTest extends PHPUnit_Framework_TestCase{ public function testDoingStuffTheRightWay() { $testedObject = new LoremIpsum(); $testedObject->setDependency(new Dependency()); $this->assertInternalType('array', $testedObject->doStuff()); }}
LET'S CHECK IF IT DOESN'T WORKclass LoremIpsumTest extends PHPUnit_Framework_TestCase{ /** * @expectedException \Exception * @expectedExceptionMessage I really need this, mate */ public function testDoingStuffTheWrongWay() { $testedObject = new LoremIpsum(); $testedObject->doStuff(); }}
ASSERTIONSAssertions have to check if the expected value corresponds
to an actuall result from the class we test.
Fufilling all of the assertions in a test means a positive result,failing to meet any of them means a negative result.
CHOICE APLENTYassertContainsassertCountassertEmptyassertEqualsassertFalseassertFileExistsassertInstanceOfassertSameassertNull...
CHOICE APLENTYassertContainsassertCountassertEmptyassertEqualsassertFalseassertFileExistsassertInstanceOfassertSameassertNull...
EXCEPTIONS:When we want to check if a method throws an exception,
instead of using assertX, we use annotations that will providethe same service.
/** * @expectedException ClassName * @expectedExceptionCode 1000000000 * @expectedExceptionMessage Exception message (no quotes!) * @expectedExceptionMessageRegExp /̂Message as regex$/ */
EXCEPTIONS:Or methods, named in the same way as annotations:
$this->expectException('ClassName');$this->expectExceptionCode(1000000000);$this->expectExceptionMessage('Exception message');$this->expectExceptionMessageRegExp('/̂Message as regex$/');
TO SUMMARIZE:1. Testing for edge cases (check for conditional expression
evaluating to both true and false)2. Testing for match of actuall and expected result3. And thrown exceptions4. We can think of unit test as a way of contract5. We don't test obvious things (PHP isn't that
untrustworthy)
WHERE DO WE GET DEPENDENCY FROM??class LoremIpsumTest extends PHPUnit_Framework_TestCase{ public function testDoingStuffTheRightWay() { $testedObject = new LoremIpsum(); $testedObject->setDependency(new Dependency()); $this->assertInternalType('array', $testedObject->doStuff()); }}
WHAT DOES THE DEPENDENCY DO?class LoremIpsumTest extends PHPUnit_Framework_TestCase{ public function testDoingStuffTheRightWay() { $testedObject = new LoremIpsum(); $testedObject->setDependency(new Dependency()); $this->assertInternalType('array', $testedObject->doStuff()); }}
We can test the dependency and make sure it returns somekind of data, but what if we pass a different object of the same
type instead?
In that case we need to check what happend if thedependency returns:
Values of different typesValues in different formatsEmpty value
How many additional classes do we need?
TEST DOUBLESObjects imitating objects of a given type
Used only to perform tests
We declare what we expect of them
We declare what they can expect of us
And see what happens
TERMINOLOGYDummy - object with methods returning null valuesStub - object with methods returning given valuesMock - as above and also having some assumptions inregard of executing the method (arguments passed, howmany times it's executed)And a lot more
YOU DON'T HAVE TO REMEMBER THAT THOUGHTerms from the previous slide are often confused, unclear or
just not used.
You should use whatever terms are clear for you and yourteam or just deal with whatever is thrown at you.
Often the test double framework will determine it for us.
LET'S MAKE A DEPENDENCY!class LoremIpsumTest extends PHPUnit_Framework_TestCase{ /** * @var \PHPUnit_Framework_MockObject_MockObject|Dependency */ private $dependencyMock;
public function setUp() { $this->dependencyMock = $this->getMockBuilder(Dependency::class) ->disableOriginalConstructor() ->setMethods(array('getResults')) ->getMock(); }}
WILL IT WORK?class LoremIpsumTest extends PHPUnit_Framework_TestCase{ public function testDoingStuffTheRighterWay() { $testedObject = new LoremIpsum(); $testedObject->setDependency($this->dependencyMock); $this->assertInternalType('array', $testedObject->doStuff()); $this->assertEmpty($testedObject->doStuff()); }}
LETS DESCRIBE OUR REQUIREMENTSclass LoremIpsumTest extends PHPUnit_Framework_TestCase{ public function testDoingStuffTheRighterWay() { $this->dependencyMock->expects($this->once()) ->method('getResults') ->willReturn(array());
$testedObject = new LoremIpsum(); $testedObject->setDependency($this->dependencyMock); $this->assertInternalType('array', $testedObject->doStuff()); $this->assertEmpty($testedObject->doStuff()); }}
WHAT IS PROVIDED IN MOCKBUILDER?Defining mocked methods
If we won't use setMethods - all methods will return nullIf we pass an array to setMethods:
Methods which names we passed can be overwrittenor will return nullMethods which names we didn't pass will behave asspecified in the mocked class
If we passed null to setMethods - all methods will behaveas specified in the mocked class
WHAT ELSE IS PROVIDED IN MOCKBUILDER?Disabling the constructorPassing arguments to the constructor (if it's public)Mocking abstract classes (if we overwrite all abstractmethods)Mocking traits
GREAT EXPECTATIONSexpects() method lets us define how many times (if ever) a
method should be executed in given conditions.
What can we pass to it?
$this->any()$this->once()$this->exactly(...)$this->never()$this->atLeast(...)...
WHAT WE CAN OFFER?with() methods allows us to inform the mock whatparameters are expected to be passed to method.
What can we pass to it?
Concrete value (or many if we have many arguments)$this->isInstanceOf(...)$this->callback(...)
WHAT DO WE EXPECT IN RETURN?willX() methods allow us to define what should be returned
from a method in given circumstances.
While previous methods were more like assertions, willX()allows us to define methods behaviour.
That allows us to test different cases without creating anyadditional classes.
SUMMARY:1. With mock objects we cas pass dependencies of a given
type without creating an actual object (isolation1)2. We can test different cases without creating any new
classes or parametrisation3. They free us from the necessity of creating dependencies
of dependencies4. Make test independent from external resources as
webservices or database5. Create additional test rules
AFTER CHANGESpublic function divideAndSuffixDependencyResults(){ if (!$this->dependency) { throw new \Exception("You need to specify the dependency"); } $result = array(); foreach ($this->dependency->getResults() as $value) { $sanitizedValue = (float)$value; if ($sanitizedValue == 0) { continue; } $sanitizedValue = 42 / $sanitizedValue; $sanitizedValue .= ' suffix'; $result[] = $sanitizedValue; } return $result;}
ANSWERpublic function testDivideByZeroIgnored() { $this->dependencyMock->expects($this->once()) ->method('getResults') ->willReturn(array(0));
$testedObject = new LoremIpsum(); $testedObject->setDependency($this->dependencyMock); $result = $testedObject->divideAndSuffixDependencyResults(); $this->assertEmpty($result); $this->assertInternalType('array', $result);}
ANOTHER ANSWERpublic function testDivideByZeroIgnored2() { $this->dependencyMock->expects($this->once()) ->method('getResults') ->willReturn(array(0,2));
$testedObject = new LoremIpsum(); $testedObject->setDependency($this->dependencyMock); $result = $testedObject->divideAndSuffixDependencyResults(); $this->assertEquals($result, array('21 suffix'));}
YET ANOTHER ANSWERpublic function testDivideByZeroIgnored3() { $this->dependencyMock->expects($this->once()) ->method('getResults') ->willReturn(array(0,2));
$testedObject = new LoremIpsum(); $testedObject->setDependency($this->dependencyMock); $result = $testedObject->divideAndSuffixDependencyResults(); $this->assertCount(1, $result);}
ORGANISING TESTLibraries should have test directory on the same level asdirectory with sourcesApplications should have test directory on the same levelas directory with modulesBootstrap.php (autoloader) and PHPUnit configurationshould be inside the test directoryDirectory hierarchy inside the test directory should be thesame as in the sources directoryIf we use different types of tests - the test directory shouldbe also divided into subdirectories (unit, integration,functional)
<!--?xml version="1.0" encoding="UTF-8"?--><phpunit bootstrap="Bootstrap.php" colors="true"> <testsuites> <testsuite name="Application"> <directory>./ApplicationTests</directory> </testsuite> </testsuites></phpunit>
ONE CLASS - AT LEAST ONE TEST SUITETwo simple rules:
1. If a few tests need a different setup than others - we shouldmove the setup operations into those tests (or extract amethod that creates that setup)
2. If many tests need a different setup than others - we shouldmove those test to a different suite and have a separatesetup
WHY SETUP() WHEN YOU CAN __CONSTRUCT()?setUp() is executed before every test, providing a "fresh"
testing environment every time.
Its counterpart is takeDown(), executed after every test.
TESTS AS DOCUMENTATIONLets call our tests a little different:
public function testDivisionAndSuffixReturnsArray WhenDependencyIsProvided();public function testDivisionAndSuffixThrowsException WhenDependencyWasNotProvided();public function testDivisionAndSuffixIgnoresResultsEquivalentToZero();public function testDivisionAndSuffixIgnoresResultsEquivalentToZero2();public function testDivisionAndSuffixIgnoresResultsEquivalentToZero3();
TESTS AS DOCUMENTATIONLets make our configuration a little different:
<!--?xml version="1.0" encoding="UTF-8"?--><phpunit bootstrap="Bootstrap.php" colors="true"> <testsuites> <testsuite name="Application"> <directory>./ApplicationTests</directory> </testsuite> </testsuites> <logging> <log type="testdox-text" target="php://stdout"> </log></logging></phpunit>
APPLICATIONTESTS\SERVICE\LOREMIPSUMDIVISIONANDSUFFIX
Returns array when dependency is providedThrows exception when dependency was not providedResults equivalent to zero are not processed
All we have to do is to change testdox-text to testdox-htmland provide a file path!
Or use --testdox-html parameter in terminal.
JUST ADVANTAGES!1. Two tasks done at once2. Clear naming convention...3. ...which also helps to decide what to test4. Plus a convention of separating test into suites
CODE COVERAGE1. Easy way to see what was already tested and what we still
have to test2. Can help with discovering dead code3. Is not a measure of test quality
CODE COVERAGE1. Easy way to see what was already tested and what we still
have to test2. Can help with discovering dead code
3. IS NOT A MEASURE OF TEST QUALITY
ANYTHING ELSE?C.R.A.P. (Change Risk Analysis and Predictions) index -
relation of cyclomatic complexity of a method to its codecoverage.
Low complexity means low risk, even without testing(getters, setters)Medium complexity risks can be countered with high codecoverageHigh complexity means that even testing won't help us
HOW TO TEST WHAT WE CAN'T REACH?abstract class AbstractIpsum{ protected $dependency; public function __construct($parameter) { if (is_object($parameter)) { $this->dependency = $parameter; } else if (is_string($parameter)) { if (class_exists($parameter)) { $this->dependency = new $parameter; } else { throw new \Exception($parameter." does not exist"); } } else { throw new \Exception("Invalid argument"); } }}
NOT THIS WAY FOR SUREpublic function testPassingObjectAsParameterAssignsObjectToProperty(){ $expectedValue = new \DateTime();
$mock = $this->getMockBuilder(AbstractIpsum::class) ->setConstructorArgs(array($expectedValue)) ->getMockForAbstractClass();
$this->assertSame($expectedValue, $mock->dependency);}
REFLECTION TO THE RESCUE!public function testPassingObjectAsParameterAssignsObjectToProperty(){ $expectedValue = new \DateTime(); $mock = $this->getMockBuilder(AbstractIpsum::class) ->setConstructorArgs(array($expectedValue)) ->getMockForAbstractClass();
$reflectedClass = new ReflectionClass(AbstractIpsum::class); $property = $reflectedClass->getProperty('dependency'); $property->setAccessible(true); $actualValue = $property->getValue($mock); $this->assertSame($expectedValue, $actualValue);}
We'll go through only one test, but it's easy to spot that thisclass have bigger potential. If we wrote more tests we could
use a different approach.
Instead of creating mock in every test we could create induring setup, don't call the constructor and use reflection to
call it in our tests.
IN ACTIONpublic function testPassingObjectAsParameterAssignsObjectToProperty2(){ $expectedValue = new \DateTime(); $mock = $this->getMockBuilder(AbstractIpsum::class) ->setConstructorArgs(array($expectedValue)) ->getMockForAbstractClass(); $mockClosure = Closure::bind( function (AbstractIpsum $abstractIpsum) { return $abstractIpsum->dependency; }, null, AbstractIpsum::class ); $actualValue = $mockClosure($mock); $this->assertSame($expectedValue, $actualValue);}
NOIt is possible in practice, but worthless. We test only the
public methods and their calls should cover private methods.
Private methods are what's called 'implementation detail' andas such should not be tested.
OK, BUT I HAVE LOTS OF LOGIC IN A PRIVATE METHOD AND IWON'T TEST IT THROUGH API
This is a hint that there's a bigger problem behind. You shouldthink if you actually shouldn't:
1. Change it to public2. Extract it to its own class (method object pattern)3. Extract it, along with similar methods, to their own class
(maybe we failed with the whole single responsibilityprinciple thing)
4. Bite the bullet and test through the API
SUMMARY1. Tests organisation - directories and files resemble the
project hierarchy2. We don't need to test a whole class in one test suite3. We can make documentation writing tests4. If we need to access private fields and methods we can use
reflection and Closure API
PHPUNIT PLUGINS
https://github.com/whatthejeff/nyancat-phpunit-resultprinter
MUTATION TESTINGTesting testsUnit tests are executed on slightly changed classesChanges are made to logic conditions, arithmeticoperations, literals, returned values, and so onShows us if code regressions were found by our testshttps://github.com/padraic/humbug
RECOMMENDED READINGxUnit PatternsPHPUnit manualMichelangelo van Dam - Your code are my tests!Marco Pivetta - Accessing private PHP class memberswithout reflection
Recommended