Refactorizando legacy code en PHP Parte 1 – ¿Existe algún test por ahí?

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

Anuncios

2 comentarios en “Refactorizando legacy code en PHP Parte 1 – ¿Existe algún test por ahí?

Comenta la entrada

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s