Leccion 70 de 75 12 min de lectura

Tests Unitarios

Los tests unitarios prueban unidades individuales de codigo en aislamiento. Aprender a escribir buenos tests unitarios mejora la calidad de tu codigo y facilita el mantenimiento a largo plazo.

¿Qué es una unidad?

Una unidad es la pieza mas pequena de codigo que tiene sentido probar de forma aislada. Normalmente es un metodo o funcion, aunque puede ser una clase completa si es simple.

PHP
<?php

declare(strict_types=1);

// Esta clase tiene varias unidades testeables
class Validador
{
    // Unidad 1: validar email
    public function esEmailValido(string $email): bool
    {
        return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
    }

    // Unidad 2: validar longitud minima
    public function tieneMinimo(string $texto, int $minimo): bool
    {
        return mb_strlen($texto) >= $minimo;
    }

    // Unidad 3: validar formato telefono
    public function esTelefonoValido(string $telefono): bool
    {
        return preg_match('/^\d{9}$/', $telefono) === 1;
    }
}

Estructura de tests bien organizados

Cada test debe probar un solo comportamiento. Si el nombre del test incluye "y", probablemente deberia ser dos tests separados.

PHP
<?php

declare(strict_types=1);

namespace Tests;

use App\Validador;
use PHPUnit\Framework\TestCase;

class ValidadorTest extends TestCase
{
    private Validador $validador;

    protected function setUp(): void
    {
        $this->validador = new Validador();
    }

    // Tests para esEmailValido
    public function testEmailValidoDevuelveTrue(): void
    {
        $this->assertTrue($this->validador->esEmailValido('user@example.com'));
    }

    public function testEmailSinArrobaDevuelveFalse(): void
    {
        $this->assertFalse($this->validador->esEmailValido('userexample.com'));
    }

    public function testEmailVacioDevuelveFalse(): void
    {
        $this->assertFalse($this->validador->esEmailValido(''));
    }

    // Tests para tieneMinimo
    public function testTextoConMinimoSuficienteDevuelveTrue(): void
    {
        $this->assertTrue($this->validador->tieneMinimo('password', 6));
    }

    public function testTextoConMinimoInsuficienteDevuelveFalse(): void
    {
        $this->assertFalse($this->validador->tieneMinimo('abc', 6));
    }
}

Testear casos límite

Los casos limite (edge cases) son valores extremos o situaciones especiales donde el codigo puede fallar. Es importante testearlos.

PHP
<?php

declare(strict_types=1);

namespace App;

class Paginador
{
    public function __construct(
        private readonly int $totalItems,
        private readonly int $itemsPorPagina
    ) {}

    public function totalPaginas(): int
    {
        if ($this->totalItems === 0) {
            return 0;
        }

        return (int) ceil($this->totalItems / $this->itemsPorPagina);
    }

    public function itemsEnPagina(int $pagina): array
    {
        $inicio = ($pagina - 1) * $this->itemsPorPagina;
        return ['inicio' => $inicio, 'limite' => $this->itemsPorPagina];
    }
}
PHP
<?php

declare(strict_types=1);

namespace Tests;

use App\Paginador;
use PHPUnit\Framework\TestCase;

class PaginadorTest extends TestCase
{
    // Casos normales
    public function testCalculaTotalPaginasCorrectamente(): void
    {
        $paginador = new Paginador(100, 10);

        $this->assertSame(10, $paginador->totalPaginas());
    }

    // Caso limite: division no exacta
    public function testRedondeoArribaConDivisionNoExacta(): void
    {
        $paginador = new Paginador(25, 10);

        $this->assertSame(3, $paginador->totalPaginas()); // 2.5 -> 3
    }

    // Caso limite: sin items
    public function testCeroItemsDevuelveCeroPaginas(): void
    {
        $paginador = new Paginador(0, 10);

        $this->assertSame(0, $paginador->totalPaginas());
    }

    // Caso limite: un solo item
    public function testUnItemDevuelveUnaPagina(): void
    {
        $paginador = new Paginador(1, 10);

        $this->assertSame(1, $paginador->totalPaginas());
    }

    // Caso limite: primera pagina
    public function testPrimeraPaginaEmpiezaEnCero(): void
    {
        $paginador = new Paginador(100, 10);

        $resultado = $paginador->itemsEnPagina(1);

        $this->assertSame(0, $resultado['inicio']);
    }
}
Casos limite comunes

Arrays vacios, strings vacios, valores cero, valores negativos, valores muy grandes, valores null (si se permiten), y primeros/ultimos elementos.

Tests de estado vs comportamiento

Puedes verificar el estado final de un objeto o el comportamiento durante la ejecucion.

PHP
<?php

declare(strict_types=1);

namespace App;

class Carrito
{
    private array $items = [];

    public function agregar(string $producto, float $precio, int $cantidad = 1): void
    {
        $this->items[] = [
            'producto' => $producto,
            'precio' => $precio,
            'cantidad' => $cantidad,
        ];
    }

    public function totalItems(): int
    {
        return array_sum(array_column($this->items, 'cantidad'));
    }

    public function subtotal(): float
    {
        $total = 0.0;
        foreach ($this->items as $item) {
            $total += $item['precio'] * $item['cantidad'];
        }
        return $total;
    }

    public function vaciar(): void
    {
        $this->items = [];
    }
}
PHP
<?php

declare(strict_types=1);

namespace Tests;

use App\Carrito;
use PHPUnit\Framework\TestCase;

class CarritoTest extends TestCase
{
    private Carrito $carrito;

    protected function setUp(): void
    {
        $this->carrito = new Carrito();
    }

    // Test de estado: verificamos el resultado final
    public function testAgregarProductoIncrementaTotalItems(): void
    {
        $this->carrito->agregar('Libro', 25.00, 2);

        $this->assertSame(2, $this->carrito->totalItems());
    }

    public function testSubtotalCalculaCorrectamente(): void
    {
        $this->carrito->agregar('Libro', 25.00, 2);
        $this->carrito->agregar('Cuaderno', 5.00, 3);

        // 25*2 + 5*3 = 50 + 15 = 65
        $this->assertSame(65.0, $this->carrito->subtotal());
    }

    public function testVaciarEliminaTodosLosItems(): void
    {
        $this->carrito->agregar('Libro', 25.00);
        $this->carrito->agregar('Cuaderno', 5.00);

        $this->carrito->vaciar();

        $this->assertSame(0, $this->carrito->totalItems());
        $this->assertSame(0.0, $this->carrito->subtotal());
    }

    public function testCarritoVacioTieneCeroItems(): void
    {
        $this->assertSame(0, $this->carrito->totalItems());
    }
}

Organizar tests en grupos

PHPUnit permite agrupar tests con atributos para ejecutarlos selectivamente:

PHP
<?php

declare(strict_types=1);

namespace Tests;

use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\Group;

class UsuarioTest extends TestCase
{
    #[Group('rapido')]
    public function testNombreNoVacio(): void
    {
        // Test rapido de ejecutar
    }

    #[Group('lento')]
    public function testEnvioEmailBienvenida(): void
    {
        // Test que tarda mas
    }
}
bash
# Ejecutar solo tests rapidos
./vendor/bin/phpunit --group rapido

# Excluir tests lentos
./vendor/bin/phpunit --exclude-group lento

Ejercicios

Ejercicio 1: Tests para clase Password

Escribe tests completos para esta clase, incluyendo casos limite:

PHP
<?php

declare(strict_types=1);

class ValidadorPassword
{
    public function esValido(string $password): bool
    {
        // Minimo 8 caracteres
        if (mb_strlen($password) < 8) {
            return false;
        }

        // Al menos una mayuscula
        if (!preg_match('/[A-Z]/', $password)) {
            return false;
        }

        // Al menos un numero
        if (!preg_match('/[0-9]/', $password)) {
            return false;
        }

        return true;
    }
}
Ver solucion
PHP
<?php

declare(strict_types=1);

use PHPUnit\Framework\TestCase;

class ValidadorPasswordTest extends TestCase
{
    private ValidadorPassword $validador;

    protected function setUp(): void
    {
        $this->validador = new ValidadorPassword();
    }

    // Casos validos
    public function testPasswordValidoDevuelveTrue(): void
    {
        $this->assertTrue($this->validador->esValido('Password123'));
    }

    // Casos de longitud
    public function testPasswordMuyCortoDevuelveFalse(): void
    {
        $this->assertFalse($this->validador->esValido('Pass1'));
    }

    public function testPasswordConExactamente8CaracteresEsValido(): void
    {
        $this->assertTrue($this->validador->esValido('Passwo1d'));
    }

    public function testPasswordCon7CaracteresEsInvalido(): void
    {
        $this->assertFalse($this->validador->esValido('Passw1d'));
    }

    // Casos de mayusculas
    public function testPasswordSinMayusculaDevuelveFalse(): void
    {
        $this->assertFalse($this->validador->esValido('password123'));
    }

    // Casos de numeros
    public function testPasswordSinNumeroDevuelveFalse(): void
    {
        $this->assertFalse($this->validador->esValido('PasswordABC'));
    }

    // Caso limite: vacio
    public function testPasswordVacioDevuelveFalse(): void
    {
        $this->assertFalse($this->validador->esValido(''));
    }
}

Ejercicio 2: Tests para clase Contador

Crea tests que verifiquen el estado del contador despues de varias operaciones:

PHP
<?php

declare(strict_types=1);

class Contador
{
    private int $valor;

    public function __construct(int $inicial = 0)
    {
        $this->valor = $inicial;
    }

    public function incrementar(): void
    {
        $this->valor++;
    }

    public function decrementar(): void
    {
        $this->valor--;
    }

    public function obtener(): int
    {
        return $this->valor;
    }

    public function reiniciar(): void
    {
        $this->valor = 0;
    }
}
Ver solucion
PHP
<?php

declare(strict_types=1);

use PHPUnit\Framework\TestCase;

class ContadorTest extends TestCase
{
    public function testContadorIniciaEnCeroPorDefecto(): void
    {
        $contador = new Contador();

        $this->assertSame(0, $contador->obtener());
    }

    public function testContadorIniciaConValorPersonalizado(): void
    {
        $contador = new Contador(10);

        $this->assertSame(10, $contador->obtener());
    }

    public function testIncrementarAumentaElValor(): void
    {
        $contador = new Contador(5);

        $contador->incrementar();

        $this->assertSame(6, $contador->obtener());
    }

    public function testDecrementarReduceElValor(): void
    {
        $contador = new Contador(5);

        $contador->decrementar();

        $this->assertSame(4, $contador->obtener());
    }

    public function testDecrementarPuedeDarValorNegativo(): void
    {
        $contador = new Contador(0);

        $contador->decrementar();

        $this->assertSame(-1, $contador->obtener());
    }

    public function testReiniciarPoneValorEnCero(): void
    {
        $contador = new Contador(100);

        $contador->reiniciar();

        $this->assertSame(0, $contador->obtener());
    }

    public function testMultiplesOperaciones(): void
    {
        $contador = new Contador();

        $contador->incrementar();
        $contador->incrementar();
        $contador->decrementar();

        $this->assertSame(1, $contador->obtener());
    }
}

Ejercicio 3: Identificar casos limite

Lista los casos limite que deberian testearse para esta funcion:

PHP
<?php

declare(strict_types=1);

function buscarEnArray(array $items, mixed $valor): ?int
{
    $indice = array_search($valor, $items, true);
    return $indice === false ? null : $indice;
}
Ver solucion

Casos limite a testear:

  • Array vacío: debería devolver null
  • Valor no encontrado: debería devolver null
  • Valor en primera posición (índice 0)
  • Valor en ultima posicion
  • Valor en posicion intermedia
  • Array con un solo elemento (encontrado)
  • Array con un solo elemento (no encontrado)
  • Valores duplicados (debería devolver primer índice)
  • Comparacion estricta (no confundir "1" con 1)

Te está gustando el curso?

Tenemos cursos premium con proyectos reales y soporte personalizado.

Descubrir cursos premium