lunes, 15 de agosto de 2016

Testeando métodos privados en PHP

La idea de este post es hacer testeo unitario de distintos métodos de una clase, aunque estos sean privados. Lo ideal sería utilizar un framework especializado, como puede ser PHPUnit, pero para simplificar voy a realizar testeo a “pelo”.

Cuando estamos testeando una clase siempre llega el momento en el que nos damos cuenta que no es posible testear un método privado de forma unitaria, pero queremos hacerlo; siempre lo estamos testeando indirectamente a través de un método público.

¿Cómo podemos testear de forma unitaria ese método privado?

La solución es utilizar reflexión (reflection).

Tenemos la siguiente clase llamada RobotCar:
[code] /** * Robot car moving along a road with several lanes. It can just go ahead and change rails left or right. * It is able to accelerate */ class RobotCar { private $x; private $y; private $acceleratorFactor; public function __construct() { $this->x = 0; // position in axe x $this->y = 0; // position in axe y $this->acceleratorFactor = 1; } public function changeToLeft ( array $viewAtLeft = array() ) { if($this->isSideFree($viewAtLeft)) $this->changeX(-1); } public function changeToRight ( array $viewAtRight = array() ) { if($this->isSideFree($viewAtRight)) $this->changeX(1); } private function changeX($mov) { $this->x += $mov; } private function isSideFree( array $view ) : bool { if(empty($view)) return true; return false; } public function goStraight() { $this->y += $this->acceleratorFactor; } public function getPosition() : string { return "X: ".$this->x." - Y: ".$this->y; } public function accelerate() { $this->acceleratorFactor += 1; } } [/code]
Esta clase representa un coche robot que, en un tablero de varios carriles (infinitos para simplificar), puede avanzar sólo hacía adelante (desplazamiento en eje y), con un factor de aceleración configurable, cambiando de carril si es necesario (desplazamiento en eje x).

Vemos que hay dos métodos, changeToLeft y changeToRight, que utilizan el mismo método privado isSideFree para verificar si es posible realizar el cambio de carril:
[code] private function isSideFree( array $view ) : bool { if(empty($view)) return true; return false; } [/code]
El método isSideFree recibe un array llamado $view que contiene todo objeto del mundo real que se ve antes de realizar el cambio y devuelve un booleano. ¿Cómo probarlo de forma unitaría?

Vamos a utilizar la clase de PHP ReflectionClass.

El primer paso es instanciar un objeto de dicha clase y extraer el método privado que vamos a utilizar.
[code] $reflected = new ReflectionClass("RobotCar"); $method = $reflected->getMethod("isSideFree"); [/code]
Ya hemos obtenido el método en la variable $method. El método es privado, por lo tanto hay que hacerlo accesible desde fuera:
[code] $method->setAccessible(true); [/code]
Ahora sólo queda invocar el método:
[code] $isFree = $method->invokeArgs(new RobotCar(), array(array("Arbol"))); [/code]
Vemos que los parámetros de invokeArgs son un objeto de la clase RobotCar y un array con los distintos parámetros del método a ejecutar. En este ejemplo pasamos un objeto recién instanciado, pero también en posible pasar un objeto instanciado anteriormente y modificado.

De esta forma, gracias a la técnica de reflexión, podemos testear métodos privados de forma unitaría como si se tratara de métodos públicos.

Probemos la clase con el siguiente código:
[code] $car = new RobotCar(); echo($car->getPosition().PHP_EOL); $car->goStraight(); echo($car->getPosition().PHP_EOL); // x = 0 $car->changeToLeft(); echo($car->getPosition().PHP_EOL); // x = -1 $car->goStraight(); $car->changeToLeft(array("Other Car")); echo($car->getPosition().PHP_EOL); // x = -1 $car->accelerate(); $car->goStraight(); $car->changeToRight(array()); // X = 0 echo($car->getPosition().PHP_EOL); // Test RobotCar::isSideFree. Is it possible? $reflected = new ReflectionClass("RobotCar"); $method = $reflected->getMethod("isSideFree"); $method->setAccessible(true); echo("\nTest private method RobotCar::isSideFree if there is a tree."); $isFree = $method->invokeArgs(new RobotCar(), array(array("Tree"))); assert(!$isFree,"There is a Tree, assertion must be false."); echo("\nTest private method RobotCar::isSideFree if there is nothing."); $isFree = $method->invoke(new RobotCar(), array()); assert($isFree, "There is nothing, assertion must be true."); echo("\nTest private method RobotCar::isSideFree if there is a dog on a big stone (test incorrect)."); $isFree = $method->invokeArgs(new RobotCar(), array(array("Big Stone", "Dog"))); assert($isFree,"There is a dog on a big stone, assertion must be false."); // The test is wrong inentionallity. [/code]
EL resultado devuelve:
[code] X: 0 - Y: 0 X: 0 - Y: 1 X: -1 - Y: 1 X: -1 - Y: 2 X: 0 - Y: 4 Test private method RobotCar::isSideFree if there is a tree. Test private method RobotCar::isSideFree if there is nothing. Test private method RobotCar::isSideFree if there is a dog on a big stone (test incorrect). Warning: assert(): There is a dog on a big stone, assertion must be false. failed in \src\test_reflection.php on line 96 [/code]
Vemos que los 2 primeros assertions no saltan ya que tanto el resultado del método a testear como el test son correctos. El tercer assert lanza un warning porque el resultado no es el esperado. Nota: Este tercer assert es incorrecto, lo correcto sería:
[code] assert( ! $isFree,"There is a dog on a big stone, assertion must be false."); [/code]
ya que se esperaría un false. He falseado el test para demostrar que este funciona correctamente y detecta el valor no esperado.

De esta forma se puede profundizar en el testeo de clases.

Recomiendo ver la documentación de la clase ReflectionClass para ver todas sus posibilidades, lo mostrado aquí es una pequeña parte.

Código en GitHub