Lección 2: Unit Testing

Las pruebas unitarias, o Unit testing, es una metodología para probar si tu código funciona como esperas. Intentando alcanzar los límites con el fin de garantizar la calidad del trabajo. Además nos proporcionan tranquilidad a la hora de añadir nuevas caractarísticas ya que nos avisa en caso de que un código anterior deje de funcionar.

Aunque a simple vista nos da la sensación de que perdemos tiempo, en realidad economiza cada línea de código; hace el proyecto más rentable. ¿Cómo es posible? A medida que se construyen los diferentes test salen al descubierto posibilidades de error no previstas por nadie, lo que lleva inevitablemente a encontrar menos errores en un futuro ergo habrá que dedicar menos horas a reparar problemas. No obstante es cierto que al principio de un proyecto la productividad es baja. Podemos verlo con el siguiente gráfico.

Gráfica de TDD contra proyecto sin testing

Tutorial con PHP

Usaremos una herramienta llamada PHPUnit. Un framework orientado a objetos para crear instancias de Unit Testing fáciles de integrar.

Primero tendremos que descargar el binario. Será el software que ejecutará las pruebas.

wget -O phpunit https://phar.phpunit.de/phpunit-8.phar

chmod +x phpunit

./phpunit --version

Ahora vamos a crear un simple objeto para gestionar la batería de un smartphone. Será capaz de:

  • Obtener el porcentaje de batería.
  • Modificar el porcentaje.
  • Saber si esta lleno.
  • Generar un texto explicativo bonito para el ojo: “Mi porcetaje es 32 %”.

Se llamará Bateria.php y dentro dispondrá del siguiente código.

<?php

//======================================================================
// Clase para gestionar la batería 
//======================================================================
class Bateria
{
    //-----------------------------------------------------
    // Variables
    //-----------------------------------------------------
    private $porcentaje = 0;

    //-----------------------------------------------------
    // GET
    //-----------------------------------------------------

    public function getPorcentaje(): int
    {
        return $this->porcentaje;
    }

    public function isLleno(): bool
    {
        return $this->porcentaje == 100;
    }

    //-----------------------------------------------------
    // SET
    //-----------------------------------------------------
    public function setPorcentaje(int $nuevoPorcentaje): void
    {
        $miPorcentaje = $nuevoPorcentaje;
        if ($nuevoPorcentaje > 100) $miPorcentaje = 100;
        if ($nuevoPorcentaje < 0) $miPorcentaje = 0;
        $this->porcentaje = $miPorcentaje;
    }

    //-----------------------------------------------------
    // Métodos
    //-----------------------------------------------------

    public function verPorcentajeBonito(): string
    {
        $miPorcentaje = $this->getPorcentaje();
        return "Mi porcetaje es $miPorcentaje %";
    }

}

Ahora definimos, copiando la plantilla de PHPUnit de su documentación, los test. Cada uno se separa con una función y dentro diferentes assets (tipo de prueba).

El archivo será llamado BateriaTest.php.

<?php

// Damos un espacio de trabajo aislado.
use PHPUnit\Framework\TestCase;

// Importamos nuestro código.
require_once('Bateria.php');

// Creamos el test
class BateriaTest extends TestCase
{
    private $miBateria = null;

    // Antes de hacer nada, creamos el objeto Bateria
    function setUp(): void
    {
        $this->miBateria = new Bateria();
    }

    // Testeamos si podemos obtener el porcentaje y entra entre el 0 y el 100
    public function testObtenerPorcentaje(): void
    {
        $porcentaje = $this->miBateria->getPorcentaje();
        $this->assertTrue(0 <= $porcentaje && $porcentaje <= 100);
    }

    // Testeamos que podamos modificarlo.
    public function testCambiarPorcentaje(): void
    {
        // Guardar
        $this->miBateria->setPorcentaje(12);
        $porcentaje = $this->miBateria->getPorcentaje();
        $this->assertSame($porcentaje, 12);
        $this->miBateria->setPorcentaje(100);
        $porcentaje = $this->miBateria->getPorcentaje();
        $this->assertSame($porcentaje, 100);
        // Valores inferiores
        $this->miBateria->setPorcentaje(-30);
        $porcentaje = $this->miBateria->getPorcentaje();
        $this->assertSame($porcentaje, 0);
        $this->miBateria->setPorcentaje(-1);
        $porcentaje = $this->miBateria->getPorcentaje();
        $this->assertSame($porcentaje, 0);
        // Valores superiores
        $this->miBateria->setPorcentaje(1230);
        $porcentaje = $this->miBateria->getPorcentaje();
        $this->assertSame($porcentaje, 100);
        $this->miBateria->setPorcentaje(101);
        $porcentaje = $this->miBateria->getPorcentaje();
        $this->assertSame($porcentaje, 100);

    }

    // Testeamos que nos devuelva si esta lleno o no
    public function testSiEstaLleno(): void
    {
        // Lleno
        $this->miBateria->setPorcentaje(100);
        $this->assertTrue($this->miBateria->isLleno());
        // No lleno
        $this->miBateria->setPorcentaje(10);
        $this->assertTrue(!$this->miBateria->isLleno());
    }

    // Testeamos que el texto bonito sea correcto
    public function testPorcentajeBonito(): void
    {
        $this->miBateria->setPorcentaje(45);
        $this->assertSame($this->miBateria->verPorcentajeBonito(), 'Mi porcetaje es 45 %');
    }
}

Para hacer el test haremos uso de setUp(), una función que es llamada antes de empezar los tests.

Existen 2 funciones para gestionar tareas que deseamos ejecutar antes o después. Antes de los tests setUp() y después tearDown(). Extremadamente útil con objetos o bases de datos.

Ejecutamos los tests.

./phpunit BateriaTest.php

Y nos informa que ha pasado los 4 tests y que ha ejecutado 10 assets correctamente.

PHPUnit 7.5.7 by Sebastian Bergmann and contributors.

    ....                                                                4 / 4 (100%)

    Time: 56 ms, Memory: 10.00 MB

    OK (4 tests, 10 assertions)

Esta noche podemos dormir en paz.

Puedes ver muchos más asserts en la documentación de PHPUnit

Data Providers

Pongamos que necesito testear una gran cantidad de resultados. En este caso la función isLleno():

Funcion Porcentaje Esperado
isLleno() 0 False
isLleno() 10 False
isLleno() 30 False
isLleno() 60 False
isLleno() 99 False
isLleno() 100 True

Toca testearlos todos. Posiblemente pensarás que tendrás mejores cosas que hacer, y no te falta razón. Por suerte no fuiste el único, por eso alguien hizo los Data Providers. Un simple sistema donde pasaremos un array con valores que serán testeados en cadena.

Observa la implementación.

<?php
use PHPUnit\Framework\TestCase;

require_once('Bateria.php');

class BateriaTest extends TestCase
{
    private $miBateria = null;

    function setUp()
    {
        $this->miBateria = new Bateria();
    }

    public function providerSiEstaLleno()
    {
        return [
            [0, False],
            [10, False],
            [30, False],
            [60, False],
            [99, False],
            [100, True],
        ];
    }

    /**
     * @dataProvider providerSiEstaLleno
     */
    public function testSiEstaLleno($porcentaje, $esperado): void
    {
        $this->miBateria->setPorcentaje($porcentaje);
        $this->assertTrue($this->miBateria->isLleno() === $esperado);
    }
}

Ejecuto mi test.

./phpunit BateriaTest.php

Y me devuelve un relajante resultado.



PHPUnit 7.5.7 by Sebastian Bergmann and contributors.

......                                                              6 / 6 (100%)

Time: 58 ms, Memory: 10.00 MB

OK (6 tests, 6 assertions)

Bases de datos

Testear nuestro códígo es relativamente fácil ya que el propio código solo responde ante sí mismo. Pero, ¿qué pasa cuando tenemos una base de datos? Aquí nos encontramos un gran problema, para saber si todo funciona como debe necesitaríamos crear, modificar y borrar mucha información. ¿Y qué pasa si algo sale mal? (que es muy probable). ¿Cómo le explicamos a nuestro jefe que hemos eliminado todos los usuarios de la tienda porque un test salió mal?

Antes de continuar debes saber que nunca nunca nunca debes testear una base de datos en producción. No importa la razon: tiempo, economizar código, simplificar… Acabará siempre en desgracia.

Pero ello no quita que hagas pruebas con datos reales. Puedes usar algunas soluciones.

Duplicar la base de datos

Copias literalmente la base de datos antes de empezar los tests.

public function setUp()
{
    copy('base_de_datos.sqlite', 'testing_base_de_datos.sqlite');
    return new PDO('sqlite:testing_base_de_datos.sqlite');
}

Cargar la base de datos en memoria

En lugar de copiarla en tu disco, realizas volcado en tu memoría RAM. Antes comprueba que tu base de datos tiene la posibilidad y en el servidor bastante RAM.

public function getConnection()
{
    $pdo = new PDO('sqlite::memory:');
    return $this->createDefaultDBConnection($pdo, ':memory:');
}

Cargar nuevos datos en tablas duplicadas

Supongamos que tienes la tabla usuarios y mensajes. Al empezar los tests copias las 2 tablas como test_usuarios y test_mensajes. Cuando terminas borras cualquier tabla que tenga el prefijo test_.

# Creamos la tabla vacia para los test
CREATE TABLE test_usuarios LIKE usuarios;
CREATE TABLE test_mensajes LIKE mensajes;

# Insertamos la misma información
INSERT test_usuarios SELECT * FROM usuarios; 
INSERT test_mensajes SELECT * FROM mensajes; 

2-1 2-2 2-3