Wie stubst man eine Methode in PHPUnit, die vom Konstruktor der zu testenden Klasse aufgerufen wird? Der folgende einfache Code funktioniert beispielsweise nicht, da das Stub-Objekt zu dem Zeitpunkt, an dem ich die Stubb-Methode deklariere, bereits erstellt und meine Methode ohne Stubb aufgerufen wurde.
Klasse zum testen:
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);
}
}
Testcode:
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);
}
}
Verwenden disableOriginalConstructor()
so dass getMock()
wird den Konstruktor nicht aufrufen. Der Name ist etwas irreführend, da der Aufruf dieser Methode endet false
zum $callOriginalConstructor
. Auf diese Weise können Sie Erwartungen an den zurückgegebenen Mock setzen, bevor Sie den Konstruktor manuell aufrufen.
$stub = $this->getMockBuilder('ClassA')
->setMethods(array('getResultFromRemoteServer'))
->disableOriginalConstructor()
->getMock();
$stub->expects($this->any())
->method('getResultFromRemoteServer')
->will($this->returnValue('Poodle'));
$stub->__construct('dog52');
...
Das Problem ist nicht das Stubbing der Methode, sondern Ihre Klasse.
Sie arbeiten im Konstruktor. Um das Objekt in den Zustand zu versetzen, holen Sie sich eine entfernte Datei. Dieser Schritt ist jedoch nicht erforderlich, da das Objekt diese Daten nicht benötigt, um in einem gültigen Zustand zu sein. Sie brauchen das Ergebnis aus der Datei nicht, bevor Sie tatsächlich anrufen getFormatted
.
Sie könnten das Laden verschieben:
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());
}
}
Sie sind also faul, den Fernzugriff zu laden, wenn er tatsächlich benötigt wird. Jetzt brauchen Sie nicht mehr zu stummeln getResultFromRemoteServer
überhaupt, kann aber stub getFormatted
stattdessen. Sie müssen Ihre API auch nicht zum Testen und Erstellen öffnen getResultFromRemoteServer
dann öffentlich.
Nebenbei bemerkt, auch wenn es nur ein Beispiel ist, ich würde diese Klasse umschreiben, um sie zu lesen
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));
}
}
Da es sich um einen Finder handelt, könnte es sinnvoller sein, ihn tatsächlich zu haben findById
jetzt öffentlich. Halten Sie es einfach geschützt, denn das hatten Sie in Ihrem Beispiel.
Die andere Option wäre die zu verlängern Subjekt im Test und ersetze die Methode getResultFromRemoteServer
wobei Ihre eigene Implementierung zurückkehrt Poodle
. Dies würde bedeuten, dass Sie nicht das Tatsächliche testen ClassA
aber eine Unterklasse von ClassA
aber das passiert, wenn Sie die Mock-API trotzdem verwenden.
Ab PHP7 könnten Sie eine anonyme Klasse wie diese verwenden:
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);
}
Vor PHP7 schrieben Sie einfach eine reguläre Klasse, die die erweitert Subjekt im Test und verwenden Sie das anstelle von Subjekt im Test. Oder verwenden disableOriginalConstructor
wie an anderer Stelle auf dieser Seite gezeigt.