unit testing with phpunit - there's life outside of tdd

106
UNIT TESTING WITH PHPUNIT Paweł Michalik

Upload: pawel-michalik

Post on 13-Apr-2017

352 views

Category:

Software


2 download

TRANSCRIPT

UNIT TESTINGWITH PHPUNIT

Paweł Michalik

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, FOR WHAT AND WITH WHAT?

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

WHAT TO TEST?

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 CAN'T TEST THESEclass LoremIpsum{ private $dependency;}

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)

WHAT TO TEST WITH?

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()); }}

UNIT TEST FOR A GIVEN CODE UNIT ARE INDEPENDENT FROMOTHER CODE UNITS

UNIT TEST FOR A GIVEN CLASS ARE INDEPENDENT FROM ITSDEPENDENCIES

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?

ZERO

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.

CASE A

CASE B

CASE C

PHPUnit comes with a mocking framework, but lets you useany other you want.

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()); }}

OH NOES! D:

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()); }}

YISS! :D

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(...)

IF A METHOD DOES NOT MEET THE EXPECTATIONS OR DOESN'TGET REQUIRED PARAMETERS THE TEST WILL FAIL!

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.

WHAT CAN WE USE?willReturn()willReturnSelf()willReturnCallback()...

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

LETS FIX LOREM IPSUM

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;}

EQUIVALENCE CLASSES

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);}

PROPER ANSWER DOESN'T MATTER IF WE ASK FOR SOMETHINGELSE THAN WE ACTUALLY HAVE TO KNOW

HOW SHOULD WE TEST?LIVE

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>

TESTS AS DOCUMENTATIONRun it and...

WE CAN MAKE IT BETTER

OR EVEN BETTER

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!

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

ONCE AGAIN

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

RUN IT AND...

HOW DID DEPENDENCY GOT INTO THIS?

ACTUAL RESULT

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.

PROBLEMS WITH REFLECTION

NO PROBLEMS WITH REFLECTION

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);}

CAN WE TEST PRIVATE METHODS THIS WAY?

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

WHAT'S NEXT?

PHPUNIT PLUGINS

https://phpunit.de/plugins.html

PHPUNIT PLUGINS

https://github.com/whatthejeff/nyancat-phpunit-resultprinter

PROPHECY

https://github.com/phpspec/prophecy

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

THE END!ANY QUESTIONS?

THANKS FOR WATCHING!http://pawelmichalik.net/presentations