The Problem
Developer (A) creates 2 classes, MyClass and Dependency:
class MyClass
{
private $dependency;
public function __construct($dependency)
{
$this->dependency = $dependency;
}
public function addNumber($number)
{
$this->dependency->addNumber($number);
}
public function getNumbers()
{
return $this->dependency->getListOfNumbers();
}
}
class Dependency
{
private $listOfNumbers = array();
public function __construct()
{
$this->addNumber('one');
}
public function addNumber($number)
{
$this->listOfNumbers[] = $number;
}
public function getListOfNumbers()
{
return $this->listOfNumbers;
}
}
Then writes a phpunit test (naughty):
class MyClassTest extends PHPUnit_Framework_TestCase
{
public function testGetNumbers()
{
$mock = Phake::mock('Dependency');
Phake::when($mock)->getListOfNumbers()->thenReturn(array('one', 'two'));
$myClass = new MyClassOne($mock);
$myClass->addNumber('one');
$myClass->addNumber('two');
$numbers = $myClass->getNumbers();
$this->assertEquals('one', $numbers[0]); // true
}
}
The test asserts that the given condition is true. Great :)
Now, Developer (B) decides to makes some changes to the Dependency class and throw an exception in case someone tries to override the default value:
class Dependency
{
private $defaultNumber = 'one';
private $listOfNumbers = array();
public function __construct()
{
$this->addNumber('one');
}
public function addNumber($number)
{
if (!empty($this->listOfNumbers) && $number === $this->defaultNumber) {
throw new InvalidArgumentException('Cannot override default number: ' . $this->defaultNumber);
}
$this->listOfNumbers[] = $number;
}
public function getListOfNumbers()
{
return $this->listOfNumbers;
}
}
Since the first test is adding the number "one" twice, this should break it, right? Nope. The test still asserts that the given condition is true. Which makes the test completely unreliable.
The Solution
The solution is to inject a real object into the class you want to test.
class MyClassTest extends PHPUnit_Framework_TestCase
{
public function testGetNumbers()
{
$dependency = new Dependency();
$myClass = new MyClassOne($dependency);
$myClass->addNumber('one'); // throws an InvalidArgumentException
$myClass->addNumber('two');
$numbers = $myClass->getNumbers();
$this->assertEquals('one', $numbers[0]);
}
}
This way you'll always find out if a change to a dependency class breaks your code or not. Testing your class in isolations doesn't mean mocking and faking all the dependencies, it means not making calls to the db, http requests and i/o operations, just to name a few.
HTH