12 claves para Escribir Pruebas Unitarias que te Harán Sentir Orgulloso

author

Herminio Heredia

hace 3 semanas

¿Te has encontrado alguna vez mirando tus pruebas unitarias con una mueca de desaprobación?

Esa sensación de que algo no está del todo bien, que el código es un poco "feo" o que tal vez no estás aprovechando al máximo las pruebas. ¡Tranquilo, no estás solo!

Escribir pruebas unitarias efectivas es una habilidad que se perfecciona con el tiempo y la práctica. Pero no te preocupes, en esta guía te revelaré las mejores prácticas para que puedas escribir pruebas unitarias en Laravel que te hagan sentir orgulloso.

Prepárate para dominar el arte de las pruebas unitarias y llevar tu código a un nuevo nivel de calidad y confianza.

¡Comencemos!

1. Un Solo Propósito: Claridad y Enfoque

Cada prueba unitaria debe tener un único objetivo. Esto significa que debe enfocarse en probar un aspecto específico de tu código, ya sea una función, un método o una clase.

Ejemplo:

// Incorrecto:  Prueba múltiples cosas en un solo test
public function test_user_can_be_created_and_updated()
{
    $user = User::factory()->create();
    $this->assertDatabaseHas('users', ['id' => $user->id]);

    $user->name = 'Jane Doe';
    $user->save();
    $this->assertDatabaseHas('users', ['name' => 'Jane Doe']);
}

// Correcto:  Separa en pruebas individuales
public function test_user_can_be_created()
{
    $user = User::factory()->create();
    $this->assertDatabaseHas('users', ['id' => $user->id]);
}

public function test_user_can_be_updated()
{
    $user = User::factory()->create();
    $user->name = 'Jane Doe';
    $user->save();
    $this->assertDatabaseHas('users', ['name' => 'Jane Doe']);
}

Beneficios:

  • Mayor claridad: Las pruebas son más fáciles de entender y mantener.
  • Mejor aislamiento de errores: Si una prueba falla, sabes exactamente qué parte del código está fallando.
  • Facilita la depuración: Encontrar la causa de un error es mucho más rápido.

2. Nombres Descriptivos: Comunicar con Precisión

El nombre de una prueba debe describir claramente lo que se está probando. Imagina que estás leyendo el nombre de la prueba sin ver el código, ¿podrías entender qué se está probando?

Ejemplo:

// Incorrecto:  Nombre genérico
public function test_user()
{
    // ...
}

// Correcto:  Nombre específico
public function test_user_can_be_created_with_valid_data()
{
    // ...
}

Beneficios:

  • Documentación instantánea: Los nombres de las pruebas actúan como documentación del código.
  • Facilita la navegación: Encontrar las pruebas relevantes es más rápido.
  • Mejora la comunicación en el equipo: Todos entienden el propósito de cada prueba.

3. Estructura AAA: Organización para la Legibilidad

La estructura AAA (Arrange, Act, Assert) es un patrón común para organizar las pruebas unitarias.

  • Arrange: Prepara el entorno de la prueba, crea los objetos necesarios y configura los datos.
  • Act: Ejecuta el código que se está probando.
  • Assert: Verifica que el resultado sea el esperado.

Ejemplo:

public function test_user_can_be_created_with_valid_data()
{
    // Arrange
    $userData = [
        'name' => 'John Doe',
        'email' => 'john.doe@example.com',
        'password' => 'password'
    ];

    // Act
    $user = User::create($userData);

    // Assert
    $this->assertInstanceOf(User::class, $user);
    $this->assertDatabaseHas('users', $userData);
}

Beneficios:

  • Mayor legibilidad: Las pruebas son más fáciles de seguir y comprender.
  • Consistencia: Todas las pruebas siguen la misma estructura, lo que facilita su mantenimiento.
  • Mejor organización: Separa claramente las diferentes etapas de la prueba.

4. Independencia Total: Evita las Dependencias

Las pruebas unitarias deben ser independientes entre sí. Esto significa que el resultado de una prueba no debe afectar el resultado de otra prueba.

Ejemplo:

// Incorrecto:  Las pruebas dependen del orden de ejecución
public function test_user_can_be_created()
{
    $user = User::factory()->create(); 
    // ...
}

public function test_user_count_is_correct()
{
    // Esta prueba depende de que se haya ejecutado la prueba anterior
    $this->assertCount(1, User::all());
}

// Correcto:  Cada prueba crea sus propios datos
public function test_user_can_be_created()
{
    $user = User::factory()->create(); 
    // ...
}

public function test_user_count_is_correct()
{
    User::factory()->create();
    $this->assertCount(1, User::all());
}

Beneficios:

  • Mayor confiabilidad: Las pruebas son más robustas y menos propensas a fallar por factores externos.
  • Facilita la ejecución en paralelo: Las pruebas se pueden ejecutar en cualquier orden o en paralelo.
  • Simplifica el mantenimiento: Los cambios en una prueba no afectan a otras pruebas.

5. Aislamiento del Entorno: Control Total

Las pruebas unitarias deben aislarse del entorno externo, como la base de datos, el sistema de archivos o las APIs externas. Esto se puede lograr utilizando mocks, stubs o bases de datos en memoria.

Ejemplo:

// Incorrecto:  La prueba depende de una API externa
public function test_user_can_be_created_with_external_api_call()
{
    // ... código que realiza una llamada a una API externa ...
}

// Correcto:  Utilizar un mock para simular la API externa
public function test_user_can_be_created_with_external_api_call()
{
    // Mock de la API externa
    $mock = Mockery::mock(ExternalApiService::class);
    $mock->shouldReceive('createUser')->andReturn(true);
    $this->app->instance(ExternalApiService::class, $mock);

    // ... código que utiliza la API externa ...
}

Beneficios:

  • Mayor velocidad de ejecución: Las pruebas se ejecutan más rápido al no depender de recursos externos.
  • Mayor confiabilidad: Las pruebas no se ven afectadas por problemas en el entorno externo.
  • Facilita la reproducción de errores: Se pueden simular diferentes escenarios y respuestas de las dependencias externas.

6. Pruebas de Frontera: No te Olvides de los Extremos

Las pruebas de frontera se enfocan en probar los límites de tu código, como valores máximos, mínimos, nulos o vacíos. Estos casos suelen ser donde se esconden los errores.

Ejemplo:

public function test_username_must_be_at_least_3_characters_long()
{
    $this->expectException(ValidationException::class);

    User::factory()->create(['name' => 'ab']); 
}

public function test_username_cannot_be_longer_than_255_characters()
{
    $this->expectException(ValidationException::class);

    User::factory()->create(['name' => str_repeat('a', 256)]); 
}

Beneficios:

  • Mayor cobertura de código: Se prueban casos que a menudo se pasan por alto.
  • Detección temprana de errores: Se encuentran errores que podrían causar problemas en producción.
  • Mayor robustez del código: El código es más resistente a entradas inesperadas.

7. Refactorización Continua: Mantén tus Pruebas Limpias

Las pruebas unitarias, al igual que el código de producción, deben refactorizarse con regularidad. Elimina código duplicado, mejora la legibilidad y mantén las pruebas actualizadas.

Ejemplo:

// Incorrecto:  Código duplicado en varias pruebas
public function test_user_can_be_created()
{
    $userData = [
        'name' => 'John Doe',
        'email' => 'john.doe@example.com',
        'password' => 'password'
    ];
    // ...
}

public function test_user_can_be_updated()
{
    $userData = [
        'name' => 'John Doe',
        'email' => 'john.doe@example.com',
        'password' => 'password'
    ];
    // ...
}

// Correcto:  Extraer el código duplicado a un método auxiliar
private function createUserData()
{
    return [
        'name' => 'John Doe',
        'email' => 'john.doe@example.com',
        'password' => 'password'
    ];
}

public function test_user_can_be_created()
{
    $userData = $this->createUserData();
    // ...
}

public function test_user_can_be_updated()
{
    $userData = $this->createUserData();
    // ...
}

Beneficios:

  • Mayor mantenibilidad: Las pruebas son más fáciles de modificar y actualizar.
  • Reducción de código duplicado: Se evita la repetición de código y se mejora la legibilidad.
  • Mayor eficiencia: Se ahorra tiempo al no tener que escribir el mismo código varias veces.

8. Pruebas "Felices" y "Tristes": Cubre Todos los Escenarios

Asegúrate de cubrir tanto los casos de éxito ("felices") como los casos de error ("tristes"). Prueba qué sucede cuando la entrada es válida, inválida, nula, vacía, etc.

Ejemplo:

// Prueba "feliz":  El usuario se crea correctamente
public function test_user_can_be_created_with_valid_data()
{
    // ...
}

// Prueba "triste":  Se lanza una excepción si el email es inválido
public function test_user_cannot_be_created_with_invalid_email()
{
   $this->expectException(ValidationException::class);

   User::factory()->create(['email' => 'invalid_email']); 
}

Beneficios:

  • Mayor confianza en el código: Se verifica que el código funciona correctamente en todas las situaciones.
  • Detección temprana de errores: Se encuentran errores que podrían pasar desapercibidos en los casos de éxito.
  • Mayor robustez del código: El código es más resistente a entradas inesperadas.

9. Utilizar Datasets: Reduce la Repetición

Si necesitas probar la misma lógica con diferentes conjuntos de datos, utiliza datasets. Esto te permite ejecutar la misma prueba con múltiples variaciones de datos sin duplicar código.

Ejemplo:

/**
 * @dataProvider validEmailProvider
 */
public function test_user_can_be_created_with_valid_email($email)
{
    $user = User::factory()->create(['email' => $email]);
    $this->assertDatabaseHas('users', ['email' => $email]);
}

public function validEmailProvider()
{
    return [
        ['john.doe@example.com'],
        ['jane.doe+test@example.com'],
        ['test.user@subdomain.example.com']
    ];
}

Beneficios:

  • Reducción de código duplicado: Se evita la repetición de código y se mejora la legibilidad.
  • Mayor eficiencia: Se ahorra tiempo al no tener que escribir la misma prueba varias veces.
  • Mayor cobertura de código: Se prueban diferentes escenarios con menos código.

10. Mocks y Stubs: Controla tus Dependencias

Utiliza mocks y stubs para simular el comportamiento de las dependencias externas. Esto te permite aislar el código que estás probando y controlar las respuestas de las dependencias.

Ejemplo:

public function test_user_can_be_created_with_external_api_call()
{
    // Mock de la API externa
    $mock = Mockery::mock(ExternalApiService::class);
    $mock->shouldReceive('createUser')->andReturn(true);
    $this->app->instance(ExternalApiService::class, $mock);

    // ... código que utiliza la API externa ...
}

Beneficios:

  • Mayor velocidad de ejecución: Las pruebas se ejecutan más rápido al no depender de recursos externos.
  • Mayor confiabilidad: Las pruebas no se ven afectadas por problemas en el entorno externo.
  • Facilita la reproducción de errores: Se pueden simular diferentes escenarios y respuestas de las dependencias externas.

11. No Te Olvides de las Pruebas de Integración

Las pruebas unitarias son importantes, pero no son suficientes. También necesitas pruebas de integración para verificar que los diferentes componentes de tu aplicación funcionan correctamente juntos.

Ejemplo:

public function test_user_can_login()
{
    $user = User::factory()->create();

    $response = $this->post('/login', [
        'email' => $user->email,
        'password' => 'password'
    ]);

    $response->assertRedirect('/home');
    $this->assertAuthenticatedAs($user);
}

Beneficios:

  • Mayor confianza en la aplicación: Se verifica que la aplicación funciona como un todo.
  • Detección temprana de errores de integración: Se encuentran errores que podrían pasar desapercibidos en las pruebas unitarias.
  • Mayor calidad del software: Se entrega un producto más robusto y confiable.

12. Herramientas para el Éxito: Aprovecha el Ecosistema Laravel

Laravel ofrece un conjunto de herramientas que facilitan la escritura de pruebas unitarias. Aprovecha PHPUnit, las factories, los seeders y otras herramientas para escribir pruebas más eficientes.

Ejemplo:

use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;

class UserTest extends TestCase
{
    use RefreshDatabase;

    public function test_user_can_be_created()
    {
        $user = User::factory()->create();
        $this->assertDatabaseHas('users', ['id' => $user->id]);
    }
}

Beneficios:

  • Mayor productividad: Escribir pruebas es más rápido y sencillo.
  • Mayor calidad de las pruebas: Se aprovechan las mejores prácticas y herramientas disponibles.
  • Mayor integración con Laravel: Las pruebas se integran perfectamente con el framework.

Conclusión: El Camino hacia la Maestría

Escribir pruebas unitarias efectivas es un viaje continuo de aprendizaje y mejora. No te desanimes si al principio te cuesta o si tus pruebas no son perfectas. Lo importante es que comiences a aplicar las mejores prácticas y que te esfuerces por mejorar con cada prueba que escribas.

Con el tiempo, desarrollarás la intuición y la experiencia necesarias para escribir pruebas unitarias impecables que te den la confianza de que tu código es sólido, confiable y está listo para enfrentar cualquier desafío.

¡Sigue practicando y no te rindas!

Herminio Heredia

¡Hola! Soy Herminio Heredia Santos, un apasionado del desarrollo web que ha encontrado en Laravel su herramienta predilecta para crear proyectos increíbles. Me encanta la elegancia y la potencia que...

Suscríbete para Actualizaciones

Proporcione su correo electrónico para recibir notificaciones sobre nuevas publicaciones o actualizaciones.