saltar hacia el contenido

Buscar

Patrón AAA en Test Unitarios

8 min read Updated:

Este patrón, divide cada test en tres fases distintas y secuenciales, facilitando la legibilidad, mantenimiento y escalabilidad de las pruebas unitarias.

# testing
Sin post relacionado

El Patrón AAA (Arrange-Act-Assert) en Tests Unitarios con PHP

Introducción al Patrón AAA

En el desarrollo de software moderno, los tests unitarios son una práctica esencial para garantizar la calidad y robustez del código. Entre las metodologías más efectivas para estructurar estos tests se encuentra el patrón AAA (Arrange-Act-Assert), que proporciona un enfoque claro y consistente para la creación de pruebas. Este patrón, divide cada test en tres fases distintas y secuenciales, facilitando la legibilidad, mantenimiento y escalabilidad de las pruebas unitarias.

Los Tres Componentes del Patrón AAA

El patrón AAA se compone de tres fases bien definidas que estructuran la lógica de cada test unitario:

  1. Arrange (Preparar): En esta fase inicial, se establecen todas las precondiciones necesarias para la prueba. Esto incluye la inicialización de objetos, configuración del entorno, preparación de datos de entrada y definición de expectativas.

  2. Act (Actuar): La fase de acción es donde se ejecuta la funcionalidad que queremos probar. Normalmente, esta fase es concisa y se limita a invocar el método o función bajo prueba con los parámetros preparados en la fase anterior.

  3. Assert (Afirmar): En la fase final, verificamos que el comportamiento del sistema bajo prueba coincide con nuestras expectativas. Aquí es donde comprobamos que los resultados obtenidos son los esperados, utilizando aserciones específicas.

Implementación Básica en PHP con PHPUnit

PHPUnit es el framework de testing más utilizado en el ecosistema PHP. Veamos cómo implementar el patrón AAA en un test básico:

<?php
 
use PHPUnit\Framework\TestCase;
 
class CalculadoraTest extends TestCase
{
    public function testSuma()
    {
        // Arrange
        $calculadora = new Calculadora();
        $a = 5;
        $b = 3;
        $resultadoEsperado = 8;
        
        // Act
        $resultado = $calculadora->sumar($a, $b);
        
        // Assert
        $this->assertEquals($resultadoEsperado, $resultado);
    }
}
 
class Calculadora
{
    public function sumar($a, $b)
    {
        return $a + $b;
    }
}

En este ejemplo, hemos separado claramente las tres fases del patrón AAA. Esta estructura hace que el test sea fácil de entender y mantener.

Manejo de Dependencias en la Fase “Arrange”

Cuando trabajamos con clases que tienen dependencias, la fase “Arrange” se vuelve más compleja. Aquí es donde los dobles de prueba (mocks, stubs) se vuelven esenciales. Veamos un ejemplo más elaborado:

<?php
 
use PHPUnit\Framework\TestCase;
 
class ServicioUsuarioTest extends TestCase
{
    public function testObtenerNombreCompleto()
    {
        // Arrange
        $repositorioMock = $this->createMock(RepositorioUsuario::class);
        $repositorioMock->method('obtenerPorId')
            ->with(1)
            ->willReturn([
                'nombre' => 'Juan',
                'apellido' => 'Pérez'
            ]);
            
        $servicio = new ServicioUsuario($repositorioMock);
        
        // Act
        $nombreCompleto = $servicio->obtenerNombreCompleto(1);
        
        // Assert
        $this->assertEquals('Juan Pérez', $nombreCompleto);
    }
}
 
interface RepositorioUsuario
{
    public function obtenerPorId($id);
}
 
class ServicioUsuario
{
    private $repositorio;
    
    public function __construct(RepositorioUsuario $repositorio)
    {
        $this->repositorio = $repositorio;
    }
    
    public function obtenerNombreCompleto($id)
    {
        $usuario = $this->repositorio->obtenerPorId($id);
        return $usuario['nombre'] . ' ' . $usuario['apellido'];
    }
}

En este caso, la fase “Arrange” implica la creación de un mock para la dependencia RepositorioUsuario, estableciendo el comportamiento esperado cuando se llama al método obtenerPorId().

Pruebas con Excepciones en el Patrón AAA

El patrón AAA también se adapta perfectamente a las pruebas que verifican el lanzamiento de excepciones. Veamos cómo estructurar este tipo de tests:

<?php
 
use PHPUnit\Framework\TestCase;
 
class ValidadorTest extends TestCase
{
    public function testValidarEmailInvalido()
    {
        // Arrange
        $validador = new Validador();
        $emailInvalido = "correo-invalido";
        
        // Act & Assert (combinados para pruebas de excepciones)
        $this->expectException(InvalidArgumentException::class);
        $this->expectExceptionMessage('Email inválido');
        
        $validador->validarEmail($emailInvalido);
    }
}
 
class Validador
{
    public function validarEmail($email)
    {
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException('Email inválido');
        }
        return true;
    }
}

Observe que en este caso, las fases “Act” y “Assert” se combinan, ya que PHPUnit requiere que las expectativas de excepciones se definan antes de la acción que las desencadena.

Aplicación del Patrón AAA en Tests de Integración

Aunque el patrón AAA fue concebido principalmente para tests unitarios, también puede aplicarse eficazmente en tests de integración. En estos casos, la fase “Arrange” a menudo implica la configuración de múltiples componentes y posiblemente la preparación de una base de datos de prueba:

<?php
 
use PHPUnit\Framework\TestCase;
 
class PedidoIntegrationTest extends TestCase
{
    private $db;
    
    protected function setUp(): void
    {
        // Inicialización de la base de datos de prueba
        $this->db = new PDO('sqlite::memory:');
        $this->db->exec('
            CREATE TABLE productos (
                id INTEGER PRIMARY KEY,
                nombre TEXT,
                precio REAL
            );
            INSERT INTO productos VALUES (1, "Producto A", 100.00);
            INSERT INTO productos VALUES (2, "Producto B", 150.00);
            
            CREATE TABLE pedidos (
                id INTEGER PRIMARY KEY,
                cliente_id INTEGER,
                fecha TEXT
            );
            
            CREATE TABLE pedido_items (
                id INTEGER PRIMARY KEY,
                pedido_id INTEGER,
                producto_id INTEGER,
                cantidad INTEGER
            );
        ');
    }
    
    public function testCrearPedidoConItems()
    {
        // Arrange
        $repositorioProducto = new RepositorioProductoSQL($this->db);
        $repositorioPedido = new RepositorioPedidoSQL($this->db);
        $servicioPedido = new ServicioPedido($repositorioPedido, $repositorioProducto);
        
        $clienteId = 1;
        $items = [
            ['producto_id' => 1, 'cantidad' => 2],
            ['producto_id' => 2, 'cantidad' => 1]
        ];
        
        // Act
        $pedidoId = $servicioPedido->crearPedido($clienteId, $items);
        
        // Assert
        $stmt = $this->db->prepare('SELECT COUNT(*) FROM pedidos WHERE id = ?');
        $stmt->execute([$pedidoId]);
        $existePedido = (bool) $stmt->fetchColumn();
        $this->assertTrue($existePedido);
        
        $stmt = $this->db->prepare('SELECT COUNT(*) FROM pedido_items WHERE pedido_id = ?');
        $stmt->execute([$pedidoId]);
        $cantidadItems = (int) $stmt->fetchColumn();
        $this->assertEquals(2, $cantidadItems);
    }
}
 
// Clases simplificadas para el ejemplo
class RepositorioProductoSQL
{
    private $db;
    
    public function __construct(PDO $db)
    {
        $this->db = $db;
    }
    
    public function obtenerPorId($id)
    {
        $stmt = $this->db->prepare('SELECT * FROM productos WHERE id = ?');
        $stmt->execute([$id]);
        return $stmt->fetch(PDO::FETCH_ASSOC);
    }
}
 
class RepositorioPedidoSQL
{
    private $db;
    
    public function __construct(PDO $db)
    {
        $this->db = $db;
    }
    
    public function guardar($clienteId, $fecha, $items)
    {
        $this->db->beginTransaction();
        
        try {
            $stmt = $this->db->prepare('INSERT INTO pedidos (cliente_id, fecha) VALUES (?, ?)');
            $stmt->execute([$clienteId, $fecha]);
            $pedidoId = $this->db->lastInsertId();
            
            foreach ($items as $item) {
                $stmt = $this->db->prepare('INSERT INTO pedido_items (pedido_id, producto_id, cantidad) VALUES (?, ?, ?)');
                $stmt->execute([$pedidoId, $item['producto_id'], $item['cantidad']]);
            }
            
            $this->db->commit();
            return $pedidoId;
        } catch (Exception $e) {
            $this->db->rollBack();
            throw $e;
        }
    }
}
 
class ServicioPedido
{
    private $repositorioPedido;
    private $repositorioProducto;
    
    public function __construct(RepositorioPedidoSQL $repositorioPedido, RepositorioProductoSQL $repositorioProducto)
    {
        $this->repositorioPedido = $repositorioPedido;
        $this->repositorioProducto = $repositorioProducto;
    }
    
    public function crearPedido($clienteId, $items)
    {
        // Validar que todos los productos existan
        foreach ($items as $item) {
            $producto = $this->repositorioProducto->obtenerPorId($item['producto_id']);
            if (!$producto) {
                throw new InvalidArgumentException("Producto no encontrado: " . $item['producto_id']);
            }
        }
        
        $fecha = date('Y-m-d H:i:s');
        return $this->repositorioPedido->guardar($clienteId, $fecha, $items);
    }
}

Este ejemplo más complejo muestra cómo el patrón AAA proporciona claridad incluso en tests de integración que involucran múltiples componentes y una base de datos.

Beneficios del Patrón AAA en la Práctica Profesional

La adopción consistente del patrón AAA en el desarrollo de tests proporciona numerosos beneficios:

  1. Legibilidad mejorada: La estructura clara separa la preparación, la acción y la verificación, haciendo que los tests sean más fáciles de entender.

  2. Facilidad de mantenimiento: Cuando un test falla, la estructura AAA ayuda a identificar rápidamente en qué fase ocurrió el problema.

  3. Documentación implícita: Un test bien estructurado actúa como documentación del comportamiento esperado del sistema.

  4. Reducción de la duplicación: La separación clara facilita la identificación de código de preparación común que puede extraerse a métodos auxiliares o al método setUp().

  5. Mejora la comunicación del equipo: Proporciona un vocabulario y estructura común para discutir sobre los tests.

Patrones Avanzados de Organización de Tests

El patrón AAA puede combinarse con otros patrones de diseño de tests para manejar escenarios más complejos. Una extensión popular es el patrón “Given-When-Then” del Behavior-Driven Development (BDD), que es esencialmente una reformulación del AAA con un enfoque más orientado al comportamiento:

<?php
 
use PHPUnit\Framework\TestCase;
 
class CarritoCompraTest extends TestCase
{
    public function testAplicarDescuentoACantidadesMayores()
    {
        // Given (Arrange)
        $carrito = new CarritoCompra();
        $carrito->agregarProducto(new Producto("Laptop", 1200.00), 1);
        $carrito->agregarProducto(new Producto("Mouse", 25.00), 5);
        
        // When (Act)
        $carrito->aplicarDescuentos();
        
        // Then (Assert)
        $this->assertEquals(1200.00, $carrito->obtenerSubtotalProducto("Laptop"));
        $this->assertEquals(112.50, $carrito->obtenerSubtotalProducto("Mouse")); // 25 * 5 * 0.9 = 112.50 (10% descuento)
        $this->assertEquals(1312.50, $carrito->obtenerTotal());
    }
}
 
class Producto
{
    private $nombre;
    private $precio;
    
    public function __construct($nombre, $precio)
    {
        $this->nombre = $nombre;
        $this->precio = $precio;
    }
    
    public function getNombre()
    {
        return $this->nombre;
    }
    
    public function getPrecio()
    {
        return $this->precio;
    }
}
 
class CarritoCompra
{
    private $items = [];
    private $descuentos = [];
    
    public function agregarProducto(Producto $producto, $cantidad)
    {
        $this->items[$producto->getNombre()] = [
            'producto' => $producto,
            'cantidad' => $cantidad,
            'subtotal' => $producto->getPrecio() * $cantidad
        ];
    }
    
    public function aplicarDescuentos()
    {
        foreach ($this->items as $nombre => $item) {
            if ($item['cantidad'] >= 5) {
                $descuento = $item['subtotal'] * 0.1; // 10% de descuento para cantidades >= 5
                $this->items[$nombre]['subtotal'] -= $descuento;
                $this->descuentos[$nombre] = $descuento;
            }
        }
    }
    
    public function obtenerSubtotalProducto($nombre)
    {
        return isset($this->items[$nombre]) ? $this->items[$nombre]['subtotal'] : 0;
    }
    
    public function obtenerTotal()
    {
        $total = 0;
        foreach ($this->items as $item) {
            $total += $item['subtotal'];
        }
        return $total;
    }
}

Esta implementación muestra cómo el vocabulario puede cambiar ligeramente para adaptarse al enfoque BDD, manteniendo la esencia del patrón AAA.