Lección 3: TDD | Curso Testing

Lección 3: TDD

TDD (Test Driven Development) 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 con la creación de una espada.

  1. Creamos la funda donde debe encajar.
  2. Forjamos una espada e intentamos introducirla. La primera ocasión no lo hará.
  3. Realizamos los golpes mínimos para darle la forma adecuada.
  4. Volvemos a intentar encajarla. En caso de que continúe sin entrar seguimos trabajando sobre ella.
  5. Ya entra suavemente.
  6. Refactorizamos quitando las impurezas, marcas del trabajo y le damos brillo.

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. Creas un test.
  2. Ejecutas todos los test anteriores y el nuevo. En la primera ocasión fallará.
  3. Escribes un simple código que lo pase.
  4. Ejecuta todos los test. Si falla volvemos al paso anterior.
  5. Refactoriza ejecuta los test en cada cambio: moviendo el código a donde debe estar, quitando código repetido, documentando, dividiendo las funciones en otras más pequeñas, etc. Si falla volvemos al paso anterior.
  6. Creamos el test de la próxima nueva característica, volviendo a empezar el ciclo.

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

JavaScript

Al igual que PHP debemos utilizar alguna herramienta. La más conocida en el ecosistema JavaScript es Jest.

La instalamos.

npm install jest

Definimos un fichero de JavaScript llamado operaciones.js. Será el código que queremos testear. Dentro albergará 2 funciones.

function sumar(num1, num2) {
  return num1 + num2;
}

function restar(num1, num2) {
  return num1 - num2;
}

// Exportamos para que pueda ser invocado desde otros lugares
module.exports = {
  sumar,
  restar
};

Ahora vamos a crear los tests. Creamos un fichero llamado operaciones.test.js con el siguiente contenido.

const {sumar, restar} = require('./operaciones');

test('1 + 2 debe ser 3', () => {
  expect(sumar(1, 2)).toBe(3);
});

test('5 - 1 debe ser 4', () => {
  expect(restar(5, 1)).toBe(4);
});

Modificamos package.json indicando como debe ejecutar el testing.

{
  "scripts": {
    "test": "jest"
  }
}

Y ejecutamos.

npm run test

Si hubiera algún problema nos lo marcaría en rojo con la descripción de lo que esperaba y lo que se ha encontrado.

3-3 3-4

Esta obra está bajo una Licencia Creative Commons Atribución-NoComercial-SinDerivadas 4.0 Internacional.

Atribución/Reconocimiento-NoComercial-SinDerivados 4.0 Internacional

¿Me invitas a un café? ☕

Puedes hacerlo usando el terminal.

ssh customer@andros.dev -p 5555

Comentarios

{{ comments.length }} comentarios

Nuevo comentario

Nueva replica  {{ formatEllipsisAuthor(replyComment.author) }}

Acepto la política de Protección de Datos.

Escribe el primer comentario