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
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
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
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
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']);
}
}
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
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
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
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
}
}
# 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
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
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
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
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
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)
Has encontrado un error o tienes una sugerencia para mejorar esta leccion?
EscribenosTe está gustando el curso?
Tenemos cursos premium con proyectos reales y soporte personalizado.
Descubrir cursos premium