martes, 20 de julio de 2010

Hacer tests de un método que crea un objeto que debemos simular (una alternativa a los partial Mock)

(Editado para corregir un error grave)

Supongamos que tenemos un método en una clase y que dentro de ese método se crea un objeto, el cual debemos simular para hacer el test. Algo así como esto:


public function getFeed($url) {
    $Socket = ClassRegistry::init('HttpSocket');
    $Socket->reset(false);
    $response = $Socket->get($url);
    ...
}


En la documentación de SimpleTest se analiza ese caso y se proponen varias soluciones. Pero en el caso de CakePHP he descubierto una que es sencilla y resuelve el problema de una forma muy eficaz y elegante.

HttpSocket es una clase muy a propósito para ser simulada, ya que requiere conectarse a un servidor y no podemos garantizar que sea posible hacerlo en la situación de test, por lo que lo lógico sería usar la simulación. Pero tal como está escrito el método, el objeto se crea y se utiliza (e incluso se destruye) en el ámbito del propio método. En la interfaz de éste no hay forma de pasar el objeto, como se puede ver, por lo que habría que reescribir el código para poder escribir el test.

ClassRegistry al rescate

Para empezar, tendremos que instanciar el objeto con ClassRegistry en lugar de con el tradicional new object() de PHP. ClassRegistry es una factoría de clases que ofrece algunos servicios interesantes. Además de crear los objetos, mantiene un registro, de modo que si una clase es instanciada varias veces, y no indicamos lo contrario, no crea un objeto nuevo cada vez, sino que devuelve el existente, ahorrando memoria.

Esto se hace con el método init, de esta forma:


$Post = ClassRegistry::init('Post');


Con este método se crea una entrada en el registro que pone la clase Post bajo la clave 'Post', devolviendo un objeto de clase Post por referencia.

Pero ClassRegistry tiene otro método que nos interesa: addObject. Este método nos permite poner en el registro cualquier objeto bajo la clave que deseemos.


ClassRegistry::addObject('Post', $MockedPost);


Para el caso que nos ocupa podemos crear el Mock que necesitamos, instanciarlo y pasarlo a ClassRegistry con la clave de la clase que estamos simulando. La próxima vez que se llame a ClassRegistry::init, devolverá el Mock.

El siguiente bloque de código muestra cómo hacerlo:


Mock::generate('HttpSocket');
$Socket = ClassRegistry::init('MockHttpSocket');
ClassRegistry::addObject('HttpSocket', $Socket);


La primera línea genera la simulación de HttpSocket con el nombre MockHttpSocket.

La segunda línea instancia la clase simulada, no es imprescindible usar ClassRegistry, pero tampoco es mala práctica.

Por último, la tercera línea, hace la magia, añadiendo el objeto que acabamos de instanciar en la clave HttpSocket del registro.

Lo anterior es la preparación. A continuación habría que establecer los valores de respuesta que necesitemos, etc.

Luego vendría la llamada para probar el método:


$result = $this->Feed->getFeed($url);


Este es, de nuevo, el fragmento del método que quería probar:


public function getFeed($url) {
    $Socket = ClassRegistry::init('HttpSocket');
    $Socket->reset(false);
    $response = $Socket->get($url);
    ...
}

Al "inyectar" en ClassRegistry la clase simulada MockHttpSocket bajo la clave HttpSocket consigo que en situación de test se utilice el Mock.

Y todo ello sin tocar el código de la clase.