Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.
UNIT TESTING
WITH PHPUNIT
Paweł Michalik
THERE'S LIFE OUTSIDE OF TDD
Hofmeister, ideals are a beautiful thing, but
over the hills is too far away.
Loose translatio...
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 f...
FOR WHAT?
Easy regression finding
Can make you more inclined to use certain good practices
(dependency injection FTW)
Help...
WITH WHAT?
PhpUnit!
There should be an installation manual
But let's be serious - nobody will remember that
WHAT TO TEST?
A UNIT OF CODE
class LoremIpsum
{
private $dependency;
public function getDependency()
{
return $this->dependency;
}
publi...
A UNIT OF CODE CONTINUED
class LoremIpsum
{
public function doStuff()
{
if (!$this->dependency) {
throw new Exception("I r...
WE CAN'T TEST THESE
class LoremIpsum
{
private $dependency;
}
WE DON'T WANT TO TEST THOSE
class LoremIpsum
{
public function getDependency()
{
return $this->dependency;
}
public functi...
WHAT DO WE NEED TO TEST HERE?
public function doStuff()
{
if (!$this->dependency) {
throw new Exception("I really need thi...
WHAT DO WE NEED TO TEST HERE?
public function doStuff()
{
if (!$this->dependency) {
throw new Exception("I really need thi...
WHAT DO WE NEED TO TEST HERE?
public function doStuff()
{
if (!$this->dependency) {
throw new Exception("I really need thi...
WHY NOT THIS?
public function doStuff()
{
if (!$this->dependency) {
throw new Exception("I really need this, mate");
}
$re...
THIS TOO, BUT...
1. Unit testing doesn't replace debugging.
2. Unit testing can make your code better, but won't really do...
LET'S CHECK IF IT WORKS
class LoremIpsumTest extends PHPUnit_Framework_TestCase
{
public function testDoingStuffTheRightWa...
LET'S CHECK IF IT DOESN'T WORK
class LoremIpsumTest extends PHPUnit_Framework_TestCase
{
/**
* @expectedException Exceptio...
ASSERTIONS
Assertions have to check if the expected value corresponds
to an actuall result from the class we test.
Fufilli...
CHOICE APLENTY
assertContains
assertCount
assertEmpty
assertEquals
assertFalse
assertFileExists
assertInstanceOf
assertSam...
CHOICE APLENTY
assertContains
assertCount
assertEmpty
assertEquals
assertFalse
assertFileExists
assertInstanceOf
assertSam...
EXCEPTIONS:
When we want to check if a method throws an exception,
instead of using assertX, we use annotations that will ...
EXCEPTIONS:
Or methods, named in the same way as annotations:
$this->expectException('ClassName');
$this->expectExceptionC...
TO SUMMARIZE:
1. Testing for edge cases (check for conditional expression
evaluating to both true and false)
2. Testing fo...
WHAT TO TEST WITH?
WHERE DO WE GET DEPENDENCY FROM??
class LoremIpsumTest extends PHPUnit_Framework_TestCase
{
public function testDoingStuff...
WHAT DOES THE DEPENDENCY DO?
class LoremIpsumTest extends PHPUnit_Framework_TestCase
{
public function testDoingStuffTheRi...
UNIT TEST FOR A GIVEN CODE UNIT ARE INDEPENDENT FROM
OTHER CODE UNITS
UNIT TEST FOR A GIVEN CLASS ARE INDEPENDENT FROM ITS
DEPENDENCIES
We can test the dependency and make sure it returns some
kind of data, but what if we pass a different object of the same
...
In that case we need to check what happend if the
dependency returns:
Values of different types
Values in different format...
ZERO
TEST DOUBLES
Objects imitating objects of a given type
Used only to perform tests
We declare what we expect of them
We dec...
TERMINOLOGY
Dummy - object with methods returning null values
Stub - object with methods returning given values
Mock - as ...
YOU DON'T HAVE TO REMEMBER THAT THOUGH
Terms from the previous slide are often confused, unclear or
just not used.
You sho...
CASE A
CASE B
CASE C
PHPUnit comes with a mocking framework, but lets you use
any other you want.
LET'S MAKE A DEPENDENCY!
class LoremIpsumTest extends PHPUnit_Framework_TestCase
{
/**
* @var PHPUnit_Framework_MockObject...
WILL IT WORK?
class LoremIpsumTest extends PHPUnit_Framework_TestCase
{
public function testDoingStuffTheRighterWay()
{
$t...
OH NOES! D:
LETS DESCRIBE OUR REQUIREMENTS
class LoremIpsumTest extends PHPUnit_Framework_TestCase
{
public function testDoingStuffThe...
YISS! :D
WHAT IS PROVIDED IN MOCKBUILDER?
Defining mocked methods
If we won't use setMethods - all methods will return null
If we p...
WHAT ELSE IS PROVIDED IN MOCKBUILDER?
Disabling the constructor
Passing arguments to the constructor (if it's public)
Mock...
GREAT EXPECTATIONS
expects() method lets us define how many times (if ever) a
method should be executed in given condition...
WHAT WE CAN OFFER?
with() methods allows us to inform the mock what
parameters are expected to be passed to method.
What c...
IF A METHOD DOES NOT MEET THE EXPECTATIONS OR DOESN'T
GET 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 circumstanc...
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. W...
LETS FIX LOREM IPSUM
AFTER CHANGES
public function divideAndSuffixDependencyResults()
{
if (!$this->dependency) {
throw new Exception("You need...
EQUIVALENCE CLASSES
ANSWER
public function testDivideByZeroIgnored() {
$this->dependencyMock->expects($this->once())
->method('getResults')
->...
ANOTHER ANSWER
public function testDivideByZeroIgnored2() {
$this->dependencyMock->expects($this->once())
->method('getRes...
YET ANOTHER ANSWER
public function testDivideByZeroIgnored3() {
$this->dependencyMock->expects($this->once())
->method('ge...
PROPER ANSWER DOESN'T MATTER IF WE ASK FOR SOMETHING
ELSE THAN WE ACTUALLY HAVE TO KNOW
HOW SHOULD WE TEST?LIVE
ORGANISING TEST
Libraries should have test directory on the same level as
directory with sources
Applications should have ...
<!--?xml version="1.0" encoding="UTF-8"?-->
<phpunit bootstrap="Bootstrap.php" colors="true">
<testsuites>
<testsuite name...
ONE CLASS - AT LEAST ONE TEST SUITE
Two simple rules:
1. If a few tests need a different setup than others - we should
mov...
WHY SETUP() WHEN YOU CAN __CONSTRUCT()?
setUp() is executed before every test, providing a "fresh"
testing environment eve...
TESTS AS DOCUMENTATION
Lets call our tests a little different:
public function testDivisionAndSuffixReturnsArray
WhenDepen...
TESTS AS DOCUMENTATION
Lets make our configuration a little different:
<!--?xml version="1.0" encoding="UTF-8"?-->
<phpuni...
TESTS AS DOCUMENTATION
Run it and...
WE CAN MAKE IT BETTER
OR EVEN BETTER
APPLICATIONTESTSSERVICELOREMIPSUMDIV
ISIONANDSUFFIX
Returns array when dependency is provided
Throws exception when depend...
All we have to do is to change testdox-text to testdox-html
and provide a file path!
Or use --testdox-html parameter in te...
JUST ADVANTAGES!
JUST ADVANTAGES!
1. Two tasks done at once
2. Clear naming convention...
3. ...which also helps to decide what to test
4. ...
CODE COVERAGE
1. Easy way to see what was already tested and what we still
have to test
2. Can help with discovering dead ...
ONCE AGAIN
CODE COVERAGE
1. Easy way to see what was already tested and what we still
have to test
2. Can help with discovering dead ...
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 it...
HOW TO TEST WHAT WE CAN'T REACH?
abstract class AbstractIpsum
{
protected $dependency;
public function __construct($parame...
NOT THIS WAY FOR SURE
public function testPassingObjectAsParameterAssignsObjectToProperty()
{
$expectedValue = new DateTim...
:(
REFLECTION TO THE RESCUE!
public function testPassingObjectAsParameterAssignsObjectToProperty()
{
$expectedValue = new Dat...
We'll go through only one test, but it's easy to spot that this
class have bigger potential. If we wrote more tests we cou...
PROBLEMS WITH REFLECTION
NO PROBLEMS WITH REFLECTION
IN ACTION
public function testPassingObjectAsParameterAssignsObjectToProperty2()
{
$expectedValue = new DateTime();
$mock ...
CAN WE TEST PRIVATE METHODS THIS WAY?
NO
It is possible in practice, but worthless. We test only the
public methods and their calls should cover private methods...
OK, BUT I HAVE LOTS OF LOGIC IN A PRIVATE METHOD AND I
WON'T TEST IT THROUGH API
This is a hint that there's a bigger prob...
SUMMARY
1. Tests organisation - directories and files resemble the
project hierarchy
2. We don't need to test a whole clas...
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 TESTING
Testing tests
Unit tests are executed on slightly changed classes
Changes are made to logic conditions, a...
RECOMMENDED READING
xUnit Patterns
PHPUnit manual
Michelangelo van Dam - Your code are my tests!
Marco Pivetta - Accessing...
THE END!
ANY QUESTIONS?
THANKS FOR WATCHING!
http://pawelmichalik.net/presentations
Unit testing with PHPUnit - there's life outside of TDD
Unit testing with PHPUnit - there's life outside of TDD
Unit testing with PHPUnit - there's life outside of TDD
Unit testing with PHPUnit - there's life outside of TDD
Unit testing with PHPUnit - there's life outside of TDD
Unit testing with PHPUnit - there's life outside of TDD
Upcoming SlideShare
Loading in …5
×

Unit testing with PHPUnit - there's life outside of TDD

541 views

Published on

Basics of PHPUnit and why is it even more relevant in legacy code. Key points of the framework are explained, along with mocking objects, test organisation, creating documentations with tests and accessing non-public objects (where applicable).

Live version with additional notes available at: http://pawelmichalik.net/presentations/unit-testing-with-phpunit?showNotes=true

Prezentacja dostępna także w języku polskim: http://pawelmichalik.net/prezentacje/testy-jednostkowe-w-phpunit?showNotes=true

Published in: Software
  • Be the first to comment

Unit testing with PHPUnit - there's life outside of TDD

  1. 1. UNIT TESTING WITH PHPUNIT Paweł Michalik
  2. 2. THERE'S LIFE OUTSIDE OF TDD Hofmeister, ideals are a beautiful thing, but over the hills is too far away. Loose translation from "The loony tales" by Kabaret Potem
  3. 3. WHAT, FOR WHAT AND WITH WHAT?
  4. 4. 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 the codebase and other tests
  5. 5. FOR WHAT? Easy regression finding Can make you more inclined to use certain good practices (dependency injection FTW) Helps catching dead code Documentation without documenting
  6. 6. WITH WHAT? PhpUnit! There should be an installation manual But let's be serious - nobody will remember that
  7. 7. WHAT TO TEST?
  8. 8. A UNIT OF CODE class LoremIpsum { private $dependency; public function getDependency() { return $this->dependency; } public function setDependency(Dependency $dependency) { $this->dependency = $dependency; } }
  9. 9. A UNIT OF CODE CONTINUED class 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; } }
  10. 10. WE CAN'T TEST THESE class LoremIpsum { private $dependency; }
  11. 11. WE DON'T WANT TO TEST THOSE class LoremIpsum { public function getDependency() { return $this->dependency; } public function setDependency(Dependency $dependency) { $this->dependency = $dependency; } }
  12. 12. 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; }
  13. 13. 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; }
  14. 14. 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; }
  15. 15. 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; }
  16. 16. 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.
  17. 17. LET'S CHECK IF IT WORKS class LoremIpsumTest extends PHPUnit_Framework_TestCase { public function testDoingStuffTheRightWay() { $testedObject = new LoremIpsum(); $testedObject->setDependency(new Dependency()); $this->assertInternalType('array', $testedObject->doStuff()); } }
  18. 18. LET'S CHECK IF IT DOESN'T WORK class LoremIpsumTest extends PHPUnit_Framework_TestCase { /** * @expectedException Exception * @expectedExceptionMessage I really need this, mate */ public function testDoingStuffTheWrongWay() { $testedObject = new LoremIpsum(); $testedObject->doStuff(); } }
  19. 19. ASSERTIONS Assertions 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.
  20. 20. CHOICE APLENTY assertContains assertCount assertEmpty assertEquals assertFalse assertFileExists assertInstanceOf assertSame assertNull ...
  21. 21. CHOICE APLENTY assertContains assertCount assertEmpty assertEquals assertFalse assertFileExists assertInstanceOf assertSame assertNull ...
  22. 22. EXCEPTIONS: When we want to check if a method throws an exception, instead of using assertX, we use annotations that will provide the same service. /** * @expectedException ClassName * @expectedExceptionCode 1000000000 * @expectedExceptionMessage Exception message (no quotes!) * @expectedExceptionMessageRegExp /^Message as regex$/ */
  23. 23. 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$/');
  24. 24. 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 result 3. And thrown exceptions 4. We can think of unit test as a way of contract 5. We don't test obvious things (PHP isn't that untrustworthy)
  25. 25. WHAT TO TEST WITH?
  26. 26. 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()); } }
  27. 27. 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()); } }
  28. 28. UNIT TEST FOR A GIVEN CODE UNIT ARE INDEPENDENT FROM OTHER CODE UNITS
  29. 29. UNIT TEST FOR A GIVEN CLASS ARE INDEPENDENT FROM ITS DEPENDENCIES
  30. 30. We can test the dependency and make sure it returns some kind of data, but what if we pass a different object of the same type instead?
  31. 31. In that case we need to check what happend if the dependency returns: Values of different types Values in different formats Empty value How many additional classes do we need?
  32. 32. ZERO
  33. 33. TEST DOUBLES Objects 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
  34. 34. TERMINOLOGY Dummy - object with methods returning null values Stub - object with methods returning given values Mock - as above and also having some assumptions in regard of executing the method (arguments passed, how many times it's executed) And a lot more
  35. 35. YOU DON'T HAVE TO REMEMBER THAT THOUGH Terms from the previous slide are often confused, unclear or just not used. You should use whatever terms are clear for you and your team or just deal with whatever is thrown at you. Often the test double framework will determine it for us.
  36. 36. CASE A
  37. 37. CASE B
  38. 38. CASE C
  39. 39. PHPUnit comes with a mocking framework, but lets you use any other you want.
  40. 40. 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(); } }
  41. 41. 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()); } }
  42. 42. OH NOES! D:
  43. 43. LETS DESCRIBE OUR REQUIREMENTS class 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()); } }
  44. 44. YISS! :D
  45. 45. WHAT IS PROVIDED IN MOCKBUILDER? Defining mocked methods If we won't use setMethods - all methods will return null If we pass an array to setMethods: Methods which names we passed can be overwritten or will return null Methods which names we didn't pass will behave as specified in the mocked class If we passed null to setMethods - all methods will behave as specified in the mocked class
  46. 46. WHAT ELSE IS PROVIDED IN MOCKBUILDER? Disabling the constructor Passing arguments to the constructor (if it's public) Mocking abstract classes (if we overwrite all abstract methods) Mocking traits
  47. 47. GREAT EXPECTATIONS expects() 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(...) ...
  48. 48. WHAT WE CAN OFFER? with() methods allows us to inform the mock what parameters 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(...)
  49. 49. IF A METHOD DOES NOT MEET THE EXPECTATIONS OR DOESN'T GET REQUIRED PARAMETERS THE TEST WILL FAIL!
  50. 50. 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 any additional classes.
  51. 51. WHAT CAN WE USE? willReturn() willReturnSelf() willReturnCallback() ...
  52. 52. 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 parametrisation 3. They free us from the necessity of creating dependencies of dependencies 4. Make test independent from external resources as webservices or database 5. Create additional test rules
  53. 53. LETS FIX LOREM IPSUM
  54. 54. AFTER CHANGES public 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; }
  55. 55. EQUIVALENCE CLASSES
  56. 56. ANSWER public 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); }
  57. 57. ANOTHER ANSWER public 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')); }
  58. 58. YET ANOTHER ANSWER public 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); }
  59. 59. PROPER ANSWER DOESN'T MATTER IF WE ASK FOR SOMETHING ELSE THAN WE ACTUALLY HAVE TO KNOW
  60. 60. HOW SHOULD WE TEST?LIVE
  61. 61. ORGANISING TEST Libraries should have test directory on the same level as directory with sources Applications should have test directory on the same level as directory with modules Bootstrap.php (autoloader) and PHPUnit configuration should be inside the test directory Directory hierarchy inside the test directory should be the same as in the sources directory If we use different types of tests - the test directory should be also divided into subdirectories (unit, integration, functional)
  62. 62. <!--?xml version="1.0" encoding="UTF-8"?--> <phpunit bootstrap="Bootstrap.php" colors="true"> <testsuites> <testsuite name="Application"> <directory>./ApplicationTests</directory> </testsuite> </testsuites> </phpunit>
  63. 63. ONE CLASS - AT LEAST ONE TEST SUITE Two simple rules: 1. If a few tests need a different setup than others - we should move the setup operations into those tests (or extract a method that creates that setup) 2. If many tests need a different setup than others - we should move those test to a different suite and have a separate setup
  64. 64. 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.
  65. 65. TESTS AS DOCUMENTATION Lets call our tests a little different: public function testDivisionAndSuffixReturnsArray WhenDependencyIsProvided(); public function testDivisionAndSuffixThrowsException WhenDependencyWasNotProvided(); public function testDivisionAndSuffixIgnoresResultsEquivalentToZero(); public function testDivisionAndSuffixIgnoresResultsEquivalentToZero2(); public function testDivisionAndSuffixIgnoresResultsEquivalentToZero3();
  66. 66. TESTS AS DOCUMENTATION Lets 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>
  67. 67. TESTS AS DOCUMENTATION Run it and...
  68. 68. WE CAN MAKE IT BETTER
  69. 69. OR EVEN BETTER
  70. 70. APPLICATIONTESTSSERVICELOREMIPSUMDIV ISIONANDSUFFIX Returns array when dependency is provided Throws exception when dependency was not provided Results equivalent to zero are not processed
  71. 71. All we have to do is to change testdox-text to testdox-html and provide a file path! Or use --testdox-html parameter in terminal.
  72. 72. JUST ADVANTAGES!
  73. 73. JUST ADVANTAGES! 1. Two tasks done at once 2. Clear naming convention... 3. ...which also helps to decide what to test 4. Plus a convention of separating test into suites
  74. 74. CODE COVERAGE 1. Easy way to see what was already tested and what we still have to test 2. Can help with discovering dead code 3. Is not a measure of test quality
  75. 75. ONCE AGAIN
  76. 76. CODE COVERAGE 1. Easy way to see what was already tested and what we still have to test 2. Can help with discovering dead code 3. IS NOT A MEASURE OF TEST QUALITY
  77. 77. RUN IT AND...
  78. 78. HOW DID DEPENDENCY GOT INTO THIS?
  79. 79. ACTUAL RESULT
  80. 80. ANYTHING ELSE? C.R.A.P. (Change Risk Analysis and Predictions) index - relation of cyclomatic complexity of a method to its code coverage. Low complexity means low risk, even without testing (getters, setters) Medium complexity risks can be countered with high code coverage High complexity means that even testing won't help us
  81. 81. 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"); } } }
  82. 82. NOT THIS WAY FOR SURE public function testPassingObjectAsParameterAssignsObjectToProperty() { $expectedValue = new DateTime(); $mock = $this->getMockBuilder(AbstractIpsum::class) ->setConstructorArgs(array($expectedValue)) ->getMockForAbstractClass(); $this->assertSame($expectedValue, $mock->dependency); }
  83. 83. :(
  84. 84. 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); }
  85. 85. We'll go through only one test, but it's easy to spot that this class have bigger potential. If we wrote more tests we could use a different approach. Instead of creating mock in every test we could create in during setup, don't call the constructor and use reflection to call it in our tests.
  86. 86. PROBLEMS WITH REFLECTION
  87. 87. NO PROBLEMS WITH REFLECTION
  88. 88. IN ACTION public 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); }
  89. 89. CAN WE TEST PRIVATE METHODS THIS WAY?
  90. 90. NO It 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' and as such should not be tested.
  91. 91. OK, BUT I HAVE LOTS OF LOGIC IN A PRIVATE METHOD AND I WON'T TEST IT THROUGH API This is a hint that there's a bigger problem behind. You should think if you actually shouldn't: 1. Change it to public 2. 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 responsibility principle thing) 4. Bite the bullet and test through the API
  92. 92. SUMMARY 1. Tests organisation - directories and files resemble the project hierarchy 2. We don't need to test a whole class in one test suite 3. We can make documentation writing tests 4. If we need to access private fields and methods we can use reflection and Closure API
  93. 93. WHAT'S NEXT?
  94. 94. PHPUNIT PLUGINS https://phpunit.de/plugins.html
  95. 95. PHPUNIT PLUGINS https://github.com/whatthejeff/nyancat-phpunit- resultprinter
  96. 96. PROPHECY https://github.com/phpspec/prophecy
  97. 97. MUTATION TESTING Testing tests Unit tests are executed on slightly changed classes Changes are made to logic conditions, arithmetic operations, literals, returned values, and so on Shows us if code regressions were found by our tests https://github.com/padraic/humbug
  98. 98. RECOMMENDED READING xUnit Patterns PHPUnit manual Michelangelo van Dam - Your code are my tests! Marco Pivetta - Accessing private PHP class members without reflection
  99. 99. THE END! ANY QUESTIONS?
  100. 100. THANKS FOR WATCHING! http://pawelmichalik.net/presentations

×