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
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
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. 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
8. A UNIT OF CODE
class LoremIpsum
{
private $dependency;
public function getDependency()
{
return $this->dependency;
}
public function setDependency(Dependency $dependency)
{
$this->dependency = $dependency;
}
}
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. WE CAN'T TEST THESE
class LoremIpsum
{
private $dependency;
}
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. 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. 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. 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.
16. 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;
}
17. 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.
18. 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());
}
}
19. 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();
}
}
20.
21.
22. 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.
25. 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$/
*/
26. 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$/');
27. 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)
29. 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());
}
}
30. 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());
}
}
31.
32. UNIT TEST FOR A GIVEN CODE UNIT ARE INDEPENDENT FROM
OTHER CODE UNITS
33. UNIT TEST FOR A GIVEN CLASS ARE INDEPENDENT FROM ITS
DEPENDENCIES
34. 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?
35. 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?
37. 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
38. 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
39. 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.
43. PHPUnit comes with a mocking framework, but lets you use
any other you want.
44. 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();
}
}
45. 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());
}
}
49. 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
50. 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
51. 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(...)
...
52. 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(...)
53. IF A METHOD DOES NOT MEET THE EXPECTATIONS OR DOESN'T
GET REQUIRED PARAMETERS THE TEST WILL FAIL!
54. 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.
55. WHAT CAN WE USE?
willReturn()
willReturnSelf()
willReturnCallback()
...
56. 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
65. 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)
69. 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
70. 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.
71. 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();
72. 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>
79. 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
80. 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
82. 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
86. 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
87. 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");
}
}
}
88. 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);
}
90. 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);
}
91. 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.
96. 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.
97. 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
98. 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
103. 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
104. RECOMMENDED READING
xUnit Patterns
PHPUnit manual
Michelangelo van Dam - Your code are my tests!
Marco Pivetta - Accessing private PHP class members
without reflection