2011-04-05 14 views
20

¿Cómo se puede encontrar un método en PHPUnit invocado por la clase bajo el constructor de la prueba? El código simple a continuación, por ejemplo, no funcionará porque para el momento en que declaro el método tropezado, el trozo de objeto ya se ha creado y mi método se ha llamado, sin unir.Anulando un método llamado por el constructor de una clase

Clase de prueba:

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

Código de ensayo:

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

Respuesta

14

El problema no es el trozo del método, sino su clase.

Estás trabajando en el constructor. Para establecer el objeto en estado, obtiene un archivo remoto. Pero ese paso no es necesario, porque el objeto no necesita que los datos estén en un estado válido. No necesita el resultado del archivo antes de llamar al getFormatted.

Se podría diferir de carga:

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

por lo que son perezosos cargar el acceso remoto a cuando es realmente necesario. Ahora no es necesario que muestre getResultFromRemoteServer, sino que puede incluir getFormatted en su lugar. Tampoco necesitará abrir su API para la prueba y hacer público getResultFromRemoteServer.

En una nota, incluso si es sólo un ejemplo, me gustaría volver a escribir esa clase para leer

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

Puesto que es un Buscador, podría tener más sentido que en realidad tienen findById pública ahora. Solo mantenerlo protegido porque eso es lo que tenía en su ejemplo.


La otra opción sería para extender el Asunto bajo prueba y reemplazar el método getResultFromRemoteServer con su propia implementación de regresar Poodle. Esto significa que no está probando el ClassA real, sino una subclase de ClassA, pero esto es lo que sucede cuando utiliza la API de Mock de todos modos.

A partir de PHP7, se podría utilizar una clase anónima como esto:

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

Antes PHP7, sólo que escribiría una clase regular extender el Asunto bajo prueba y el uso que en lugar de la Subject-Under-Test. O use disableOriginalConstructor como se muestra en otra parte de esta página.

+0

Te escucho y acepto. El código de arriba acabo de aumentar por la facilidad de la ilustración, pero refleja el código en el que estoy trabajando. jontyc

+1

Veré cómo hacer clic en la llamada remota más tarde se ajusta al código real. Era solo una de esas situaciones en las que originalmente no tenía una clase porque no era necesaria, pero la añadí por la facilidad de las pruebas y las burlas. – jontyc

+0

@stebbo no dude en pasar por el chat si tiene alguna pregunta adicional. – Gordon

41

Uso disableOriginalConstructor() modo que getMock() no llamará al constructor. El nombre es un poco engañoso porque llamar a ese método termina pasando false por $callOriginalConstructor. Esto le permite establecer expectativas en el simulacro devuelto antes de llamar al constructor manualmente.

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

Eso suena perfecto. Probablemente me reestructuraré como sugirió Gordon, pero lo tendré en cuenta. Gracias de nuevo David. – jontyc

+0

B-e-a-utiful!Este problema exacto causó una cascada de errores que reduje a mi método burlado que no se llama desde el constructor. Esta respuesta resuelve la pregunta de @ jontyc (y mi) perfectamente. ¡Gracias! –

+0

exactamente lo que me encuentro. –

Cuestiones relacionadas