Otras veces en el blog hemos hablado de tests, mejores patrones para test y hemos ido dando algunos trucos para poder refactorizar aplicaciones. Hoy damos un paso más y vamos a ir poco a poco refactorizando una aplicacion en PHP, con tests y buenas prácticas.
Desafortunadamente todos hemos tenido que trabajar con código legacy, codigo antiguo, poco legible, código sin tests… así que va siendo hora de aprender a refactorizar aplicaciones legacy. Existen muchos libros que hablan sobre legacy code, en esta serie de posts vamos a basarnos en el proyecto Legacy code retreat y tomaremos como referencia los libros Working Effectively with Legacy Code y Modernizing Legacy Applications in PHP
La aplicación que vamos a refactorizar se basa en el juego del Trivial diseñado, esta aplicación legacy ha sido desarrollada por J.B. Rainsberger para los eventos “Legacy Code Retreat”. Para empezar he creado una pequeño proyecto que irá evolucionando hasta refactorizarse. Aquí dejo un link (https://github.com/jeslopcru/LegacyCode)
Entendiendo el proyecto
Antes de empezar a tocar código debemos entender un poco el proyecto. Lo primero que podemos ver es que no tiene tests. Eso es un problema, ya que si tocamos algo no sabremos si hemos roto algo, o si el proyecto deja de funcionar como debería.
Viendo los ficheros tenemos 2 Game.php
y GameRunner.php
, así que lo primero que vamos a hacer es echarle un vistazo rápido al código de GameRunner y ejecutarlo:
$ php GameRunner.php
Chet was added
They are player number 1
Pat was added
They are player number 2
Sue was added
// Algunas lineas han sido borradas para que el post no fuese demasiado largo
Pat is the current player
They have rolled a 5
Pat's new location is 3
The category is Rock
Rock Question 6
Answer was corrent!!!!
Pat now has 6 Gold Coins.
Bueno, ya tenemos la salida del juego. Como vemos tenemos un juego de trivial en el que participan 3 Jugadores Chet, Pat y Sue, en el transcurso del juego se van haciendo preguntas de distintas temáticas y ganando puntos si estas se aciertan. El primer jugador que llegue a 6 puntos gana.
Primer vistazo al código
Lo primero vamos a ver un poco el código de GameRunner.php
<?php
include __DIR__ . '/Game.php';
$notAWinner;
$aGame = new Game();
$aGame->add("Chet");
$aGame->add("Pat");
$aGame->add("Sue");
do {
$aGame->roll(rand(0, 5) + 1);
if (rand(0, 9) == 7) {
$notAWinner = $aGame->wrongAnswer();
} else {
$notAWinner = $aGame->wasCorrectlyAnswered();
}
} while ($notAWinner);
Viendo que el código no es demasiado largo ni complejo, nuestro primer paso será Code Style. Vamos a formatear el código para que sea un poco más legible. Lo mejor es utilizar un estilo de código estándar como PSR1/PSR2, Zend, Symfony,…
Ahoremos lo mismo con Game.php
, formateamos el código y hacemos un commit. Para trabajar con legacy code debemos tener claro que hay que ir dando pequeños pasos (baby step).
¿Test?
En este momento estamos echando en falta tests, Los métodos de Game.php
son algo complejos y confusos, así que no podemos cambiar código sin estar seguros de no alterar el comportamiento del mismo.
Como el código es complejo, tenemos que darle otro enfoque a nuestros tests, así que lo que vamos a hacer es ejecutar el código un montón de veces y guardar la salidas.
Tareas automáticas
Podríamos ejecutar a mano muchas veces el código php GameRunner.php
e ir guardando la salidas, pero quizás sea mejor escribir un pequeño script que automatiza esta tarea.
Vamos a crear un pequeño test con PHPUnit que ejecute GameRunner, para ello creamos una nueva carpeta en la raíz del proyecto llamada Test y dentro un nuevo fichero para los tests.
<?php
class GameRunnerTest extends PHPUnit_Framework_TestCase
{
public function testOutput()
{
ob_start();
require_once __DIR__ . '/../GameLegacy/GameRunner.php';
$output = ob_get_contents();
ob_end_clean();
var_dump($output);
}
}
Como podemos ver el test no tiene ningún assert, en realidad no es un test de verdad, solo es una manera de ejecutar el código y mostrarlo por pantalla. Cada vez que ejecutamos el código obtenemos una salida diferente.
Plantando una semilla
Analizando el código de php GameRunner.php
vemos que se utiliza la función rand() para generar números aleatorios. El primer paso que debemos hacer es controlar la generación de números aleatorios para así tener salidas similares.
con srand()
podemos indicar una semilla a partir de la cual se generen los números aleatorios. por lo que modificamos un poco el test así:
public function testOutput()
{
ob_start();
srand(1);
require_once __DIR__ . '/../GameLegacy/GameRunner.php';
$output = ob_get_contents();
ob_end_clean();
var_dump($output);
}
Ahora la salida es siempre la misma, así que ya podemos hacer un test.
Cuidado con los require e includes Si extraemos el código de la generación a un método nos encontraremos con algunos problemas en el require_once, así que deberemos llevar el require_once a la clase.
Ejecutando el código muchas veces.
Ahora que ya tenemos un pequeño test para ejecutar el código una vez podemos crear otro test para generar el código muchas veces. Así podremos tener salida del programa muchas veces y poder compararla por si en algún cambio cometemos algún error. Debemos tener en cuenta que no tenemos ningún test y hay que ir paso a paso.
function testGenerateOutput()
{
$this->generateManyOutputs(20, '/tmp/LegacyGameOutputA.txt');
$this->generateManyOutputs(20, '/tmp/LegacyGameOutputB.txt');
$outputA = file_get_contents('/tmp/LegacyGameOutputA.txt');
$outputB = file_get_contents('/tmp/LegacyGameOutputB.txt');
$this->assertEquals($outputA, $outputB);
}
private function generateManyOutputs($times, $fileName)
{
$itsFirst = true;
while ($times) {
if ($itsFirst) {
file_put_contents($fileName, $this->generateOutput());
$itsFirst = false;
} else {
file_put_contents($fileName, $this->generateOutput(), FILE_APPEND);
}
$times--;
}
}
protected function generateOutput()
{
ob_start();
srand(0);
require __DIR__ . '/../GameLegacy/GameRunner.php';
$output = ob_get_contents();
ob_end_clean();
return $output;
}
Bueno hemos creado un método que recibe 2 parámetros el numero de veces que queremos ejecutar el test y la ruta al archivo donde queremos guardar la salida del programa.
Para poder hacer esto hemos tenido que refactorizas un poco los tests añadiendo un require en la función generateOutput y modificando el archivo GameRunner.php
poniendo un require_once en vez de require.
Pero esto no es todo, si nos fijamos todos los intentos son iguales y eso no es bueno. Necesitamos ir modificando la semilla en cada test, así que vamos a dar un pasito más para tener salidas distintas.
El objetivo final es tener al menos 20000 iteraciones del juego para poder refactorizar todo nuestro legacy code de la manera más tranquila posible 🙂
protected function generateOutput($seed)
{
ob_start();
srand($seed);
require __DIR__ . '/../GameLegacy/GameRunner.php';
$output = ob_get_contents();
ob_end_clean();
return $output;
}
Ya tenemos distintas semillas para tener diferentes salidas, así que ahora solo tenemos que generar la salida 20000 veces y listo. Pero cuidado, quizás necesitemos mucha memoria para ejecutarla, así que lo mejor será modificar un poco el assert $this->assertTrue($outputA == $outputB);
para evitar el exceso de consumo de memoria.
Listo, ya hemos terminado. Tenemos 20000 salidas distintas para probar el juego, ahora tenemos tests y podemos refactorizar sin miedo a romper nada. A partir de ahora lo que haremos tener el archivo LegacyCodeA.txt intacto e ir generando dinstantas salidas para ir probando.
Conclusiones
Los primeros pasos a la hora de refactorizar son los más difíciles, ya que necesitamos encontrar alguna manera de poder comprobar que cualquier cambio que vayamos ha realizar no rompe nada para empezar a refactorizar.
Este post se basa en el proyecto Legacy code retreat y en la serie de post de Patkos Csaba Refactoring Legacy Code
2 comentarios en “Refactorizando legacy code en PHP Parte 1 – ¿Existe algún test por ahí?”