Stubbing a method called by a class' constructor
How does one stub a method in PHPUnit that is called by the class under test's constructor? The simple code below for example won't work because by the time I declare the stubbed method, the stub object has already been created and my method called, unstubbed.
Class to test:
class ClassA {
private $dog;
private $formatted;
public function __construct($param1) {
$this->dog = $param1;
$this->getResultFromRemoteServer();
}
// Would normally be private, made public for stubbing
public getResultFromRemoteServer() {
$this->formatted = file_get_contents('http://whatever.com/index.php?'.$this->dog);
}
public getFormatted() {
return ("The dog is a ".$this->formatted);
}
}
Test code:
class ClassATest extends PHPUnit_Framework_TestCase {
public function testPoodle() {
$stub = $this->getMockBuilder('ClassA')
->setMethods(array('getResultFromRemoteServer'))
开发者_运维百科 ->setConstructorArgs(array('dog52'))
->getMock();
$stub->expects($this->any())
->method('getResultFromRemoteServer')
->will($this->returnValue('Poodle'));
$expected = 'This dog is a Poodle';
$actual = $stub->getFormatted();
$this->assertEquals($expected, $actual);
}
}
Use disableOriginalConstructor()
so that getMock()
won't call the constructor. The name is a bit misleading because calling that method ends up passing false
for $callOriginalConstructor
. This allows you to set expectations on the returned mock before calling the constructor manually.
$stub = $this->getMockBuilder('ClassA')
->setMethods(array('getResultFromRemoteServer'))
->disableOriginalConstructor()
->getMock();
$stub->expects($this->any())
->method('getResultFromRemoteServer')
->will($this->returnValue('Poodle'));
$stub->__construct('dog52');
...
The problem is not the stubbing of the method, but your class.
You are doing work in the constructor. In order to set the object into state, you fetch a remote file. But that step is not necessary, because the object doesn't need that data to be in a valid state. You dont need the result from the file before you actually call getFormatted
.
You could defer loading:
class ClassA {
private $dog;
private $formatted;
public function __construct($param1) {
$this->dog = $param1;
}
protected getResultFromRemoteServer() {
if (!$this->formatted) {
$this->formatted = file_get_contents(
'http://whatever.com/index.php?' . $this->dog
);
}
return $this->formatted;
}
public getFormatted() {
return ("The dog is a " . $this->getResultFromRemoteServer());
}
}
so you are lazy loading the remote access to when it's actually needed. Now you dont need to stub getResultFromRemoteServer
at all, but can stub getFormatted
instead. You also won't need to open your API for the testing and make getResultFromRemoteServer
public then.
On a sidenote, even if it's just an example, I would rewrite that class to read
class DogFinder
{
protected $lookupUri;
protected $cache = array();
public function __construct($lookupUri)
{
$this->lookupUri = $lookupUri;
}
protected function findById($dog)
{
if (!isset($this->cache[$dog])) {
$this->cache[$dog] = file_get_contents(
urlencode($this->lookupUri . $dog)
);
}
return $this->cache[$id];
}
public function getFormatted($dog, $format = 'This is a %s')
{
return sprintf($format, $this->findById($dog));
}
}
Since it's a Finder, it might make more sense to actually have findById
public now. Just keeping it protected because that's what you had in your example.
The other option would be to extend the Subject-Under-Test and replace the method getResultFromRemoteServer
with your own implementation returning Poodle
. This would mean you are not testing the actual ClassA
, but a subclass of ClassA
, but this is what happens when you use the Mock API anyway.
As of PHP7, you could utilize an Anonymous class like this:
public function testPoodle() {
$stub = new class('dog52') extends ClassA {
public function getResultFromRemoteServer() {
return 'Poodle';
}
};
$expected = 'This dog is a Poodle';
$actual = $stub->getFormatted();
$this->assertEquals($expected, $actual);
}
Before PHP7, you'd just write a regular class extending the Subject-Under-Test and use that instead of the Subject-Under-Test. Or use disableOriginalConstructor
as shown elsewhere on this page.
精彩评论