php ereditarietà Stubing di un metodo chiamato da un costruttore di classe




php classi ereditarietà (2)

Come si fa uno stub di un metodo in PHPUnit che viene chiamato dalla classe sotto il costruttore del test? Il semplice codice seguente, ad esempio, non funzionerà perché nel momento in cui dichiaro il metodo stub, l'oggetto stub è già stato creato e il mio metodo chiamato, nonstoppato.

Classe da testare:

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);
  }
}

Codice di prova:

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);
  }
}

Il problema non è lo stub del metodo, ma la tua classe.

Stai lavorando nel costruttore. Per impostare l'oggetto in stato, si preleva un file remoto. Ma quel passaggio non è necessario, perché l'oggetto non ha bisogno che i dati siano in uno stato valido. Non è necessario il risultato del file prima di chiamare effettivamente getFormatted .

È possibile posticipare il caricamento:

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());
  }
}

quindi sei pigro a caricare l'accesso remoto quando è effettivamente necessario. Ora non è necessario eseguire lo stub getResultFromRemoteServer , ma è possibile getFormatted stub getFormatted . Inoltre, non sarà necessario aprire l'API per il test e rendere pubblico getResultFromRemoteServer .

In un sidenote, anche se è solo un esempio, vorrei riscrivere quella classe da leggere

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));
    }
}

Dal momento che si tratta di un Finder, potrebbe essere più sensato avere effettivamente findById pubblico ora. Basta tenerlo protetto perché è quello che hai avuto nel tuo esempio.

L'altra opzione sarebbe quella di estendere il Subject-Under-Test e sostituire il metodo getResultFromRemoteServer con la propria implementazione restituendo Poodle . Ciò significherebbe che non stai testando l'effettivo ClassA , ma una sottoclasse di ClassA , ma questo è ciò che accade quando usi comunque l'API Mock.

A partire da PHP7, è possibile utilizzare una classe anonima come questa:

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);
}

Prima di PHP7, dovresti semplicemente scrivere una classe regolare che estende l' oggetto- Sottotest e usarla al posto del Sottotest soggetto . Oppure usa disableOriginalConstructor come mostrato altrove in questa pagina.


Utilizzare disableOriginalConstructor() modo che getMock() non chiami il costruttore. Il nome è un po 'fuorviante perché chiamare quel metodo finisce per passare false per $callOriginalConstructor . Ciò ti consente di impostare le aspettative sulla simulazione restituita prima di chiamare manualmente il costruttore.

$stub = $this->getMockBuilder('ClassA')
             ->setMethods(array('getResultFromRemoteServer'))
             ->disableOriginalConstructor()
             ->getMock();
$stub->expects($this->any())
     ->method('getResultFromRemoteServer')
     ->will($this->returnValue('Poodle'));
$stub->__construct('dog52');
...




phpunit