Stubbing einer Methode, die vom Konstruktor einer Klasse aufgerufen wird

Lesezeit: 4 Minuten

Benutzer-Avatar
Jontyc

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 klingt super. Ich werde wahrscheinlich umstrukturieren, wie Gordon es vorgeschlagen hat, aber ich werde es im Hinterkopf behalten. Nochmals vielen Dank David.

    – Jontyc

    5. April 2011 um 7:36 Uhr

  • Schön! Genau dieses Problem verursachte eine Kaskade von Fehlern, die ich darauf beschränkte, dass meine verspottete Methode nicht vom Konstruktor aufgerufen wurde. Diese Antwort löst die (und meine) Frage von @jontyc perfekt. Vielen Dank!

    – Jess Telford

    20. September 2011 um 3:45 Uhr

  • genau das was ich finde.

    – Édipo Costa Rebouças

    9. Dezember 2016 um 13:48 Uhr

Benutzer-Avatar
Gordon

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 ClassAaber eine Unterklasse von ClassAaber 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.

  • Ich höre Sie und ich stimme zu. Den obigen Code habe ich nur zur einfacheren Veranschaulichung erstellt, aber er spiegelt den Code wider, an dem ich arbeite.

    – Jontyc

    5. April 2011 um 7:26 Uhr


  • Ich werde sehen, wie sich das Popping des Remote-Aufrufs später in den echten Code einfügt. Es war nur eine dieser Situationen, in denen ich ursprünglich keine Klasse hatte, weil sie nicht benötigt wurde, aber sie wegen der Einfachheit des Testens und Spottens hinzugefügt hatte.

    – Jontyc

    5. April 2011 um 7:33 Uhr

  • @stebbo Sie können gerne im Chat vorbeischauen, wenn Sie weitere Fragen haben.

    – Gordon

    5. April 2011 um 7:35 Uhr

  • Dies ist KEINE Antwort auf die ursprüngliche Frage. „Du solltest stattdessen Y machen und dann musst du X nicht machen“ ist KEINE Antwort auf „Wie mache ich X?“.

    – Szczepan Hołyszewski

    31. März 2017 um 14:46 Uhr

  • @Szczepan stimmt, aber das ist irrelevant, da die gezeigte Alternative sauberer ist und das OPs-Problem gelöst hat.

    – Gordon

    31. März 2017 um 15:19 Uhr

1043230cookie-checkStubbing einer Methode, die vom Konstruktor einer Klasse aufgerufen wird

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy