Lección 3: TDD

TDD (Test Driven Delevlopment) es la práctica de programación sobre testing más utilizada por desarrolladores. Consiste en seguir un diseño trabajo opuesto al testing tradicional escribiendo primero la prueba y solo añadir nuevo código si falla. Posiblemente no tendrás claro su razón de ser por lo abstracto de su naturaleza. Veamos el ciclo el clico.

  1. Prueba (test) para el requisito.
  2. Ejecuta el test.
  3. Verificar que la prueba falla.
  4. Escribir el código para pasar la prueba.
  5. Ejecuta el test.
  6. Comprobar que el código pasa la prueba.

Vamos a un ejemplo. Imaginemos que tenemos un rico bizcocho recién sacado del horno, con una forma cilindrica. Necesitamos cortarlo de tal modo que nos quede un lado completamente liso.

  1. Dibujamos en un folio la forma que deseamos.
  2. Comprobamos si el bizcocho tiene en estos momentos la misma forma. No la tiene, ha fallado nuestra prueba.
  3. Cortamos la tarta por la mitad.
  4. Comprobamos si el bizcocho ahora si tiene la forma. ¡Coinciden! Ha pasado felizmente el test.

TDD pasos

Flujo

Más que un framework TDD es una metodología o guia para realizar buen testing. Posee un flujo de trabajo muy estricto que nos empuja a dar una prioridad mayor a la prueba que al código, donde en cada pasada se crea otra prueba hasta que no puedes añadir más validaciones.

TDD flujo

  1. Inicias una función
  2. Creas el test.
  3. Ejecutas el test.
  4. ¿Lo pasa? Vuelves al punto 2 y creas el siguiente test. ¿Falla? Siguiente punto.
  5. Refactorizas o cambias el código.
  6. Ejecutas el test.
  7. ¿Lo pasa? Vuelves al punto 2. ¿Falla? Vuelves al punto 5. ¿No puedes hacer más test? Siguiente punto.
  8. Fin de la función.

Ejemplo

Vamos a escribir un método que nos diga el estado del agua (sólido, líquido o gaseoso) dependiendo de la temperatura que le demos como parámetro.

1. Inicias una función

Creo un archivo llamado Agua.php con mi código mínimo.

<?php

//======================================================================
// Clase para Agua
//======================================================================
class Agua
{
    //-----------------------------------------------------
    // GET
    //-----------------------------------------------------

    /**
     * Método que te indica el estado del agua
     * @param {float} $temperatura - Temperatura
     * @return {string} - 'Sólido', 'Líquido' o 'Gaseoso'. Devuelve NULL si tiene un argumento inválido.
     */
    function getEstado(float $temperatura): string
    {
        return '';
    }
}

2. Creas el test.

Creo un archivo llamado AguaTest.php con un test. Es este caso comprobaremos si nos devuelve Sólido entre -273 grados centígrados (el mínimo) y 0.

<?php
use PHPUnit\Framework\TestCase;

require_once('Agua.php');

class AguaTest extends TestCase
{
    private $miAgua = null;

    public function setUp(): void
    {
        $this->miAgua = new Agua();
    }

    public function testSolido(): void
    {
        foreach (range(-273, 0) as $temperatura) {
            $estado = $this->miAgua->getEstado($temperatura);
            $this->assertSame('Sólido', $estado, "Temperatura $temperatura no es Sólida");
        }
    }
}

3. Ejecutas el test.

./phpunit AguaTest.php

4. ¿Falla? Siguiente punto.

5. Refactorizas o cambias el código.

Refactorizo mi código para que pase el test.

<?php

//======================================================================
// Clase para Agua
//======================================================================
class Agua
{
    //-----------------------------------------------------
    // GET
    //-----------------------------------------------------

    /**
     * Método que te indica el estado del agua
     * @param {float} $temperatura - Temperatura
     * @return {string} - 'Sólido', 'Líquido' o 'Gaseoso'. Devuelve NULL si tiene un argumento inválido.
     */
    function getEstado(float $temperatura): string
    {
        if ($temperatura <= 0) return 'Sólido';
    }
}

6. Ejecutas el test.

./phpunit AguaTest.php

¡Lo pasa! Ahora hago el siguiente test. ¿Líquido?

<?php
use PHPUnit\Framework\TestCase;

require_once('Agua.php');

class AguaTest extends TestCase
{
    private $miAgua = null;

    public function setUp(): void
    {
        $this->miAgua = new Agua();
    }

    public function testSolido(): void
    {
        foreach (range(-273, 0) as $temperatura) {
            $estado = $this->miAgua->getEstado($temperatura);
            $this->assertSame('Sólido', $estado, "Temperatura $temperatura no es Sólida");
        }
    }

    public function testLiquida(): void
    {
        foreach (range(1, 99) as $temperatura) {
            $estado = $this->miAgua->getEstado($temperatura);
            $this->assertSame('Líquido', $estado, "Temperatura $temperatura no es Líquido");
        }
    }
}

Ejecuto el test…

./phpunit AguaTest.php

…y falla, lógico. Refactorizamos de nuevo.

<?php

//======================================================================
// Clase para Agua
//======================================================================
class Agua
{
    //-----------------------------------------------------
    // GET
    //-----------------------------------------------------

    /**
     * Método que te indica el estado del agua
     * @param {float} $temperatura - Temperatura
     * @return {string} - 'Sólido', 'Líquido' o 'Gaseoso'. Devuelve NULL si tiene un argumento inválido.
     */
    function getEstado(float $temperatura): string
    {
        if ($temperatura <= 0) return 'Sólido';
        if (0 < $temperatura && $temperatura < 100) return 'Líquido';
    }
}

Ejecuto otra vez el test.

./phpunit AguaTest.php

¡Lo pasa! A por el siguiente test… la rueda continua girando hasta tener todos.

7. ¿No puedes hacer más test? Siguiente punto.

Mis pruebas están completas.

<?php
use PHPUnit\Framework\TestCase;

require_once('Agua.php');

class AguaTest extends TestCase
{
    private $miAgua = null;

    public function setUp(): void
    {
        $this->miAgua = new Agua();
    }

    public function testSolido(): void
    {
        foreach (range(-273, 0) as $temperatura) {
            $estado = $this->miAgua->getEstado($temperatura);
            $this->assertSame('Sólido', $estado, "Temperatura $temperatura no es Sólida");
        }
    }

    public function testLiquida(): void
    {
        foreach (range(1, 99) as $temperatura) {
            $estado = $this->miAgua->getEstado($temperatura);
            $this->assertSame('Líquido', $estado, "Temperatura $temperatura no es Líquido");
        }
    }

    public function testGaseoso(): void
    {
        foreach (range(100, 500) as $temperatura) {
            $estado = $this->miAgua->getEstado($temperatura);
            $this->assertSame('Gaseoso', $estado, "Temperatura $temperatura no es Gaseoso");
        }
    }

    public function testTipoFloatOInt(): void
    {
            $estado = $this->miAgua->getEstado($temperatura);
            $this->assertSame('Gaseoso', $estado, "Temperatura $temperatura no es Gaseoso");
    }
}

8. Fin de la función.

Mi clase esta terminada y testeada.

<?php

//======================================================================
// Clase para Agua
//======================================================================
class Agua
{
    //-----------------------------------------------------
    // GET
    //-----------------------------------------------------

    /**
     * Método que te indica el estado del agua
     * @param {float} $temperatura - Temperatura
     * @return {string} - 'Sólido', 'Líquido' o 'Gaseoso'. Devuelve NULL si tiene un argumento inválido.
     */
    function getEstado(float $temperatura): string
    {
        if (is_float($temperatura) || is_int($temperatura)) {
            if ($temperatura <= 0) return 'Sólido';
            if (0 < $temperatura && $temperatura < 100) return 'Líquido';
            if (100 <= $temperatura) return 'Gaseoso';
        } else {
            return NULL;
        }
    }
}

3-1 3-2