Hablar de pruebas automatizadas es casi un mantra en el desarrollo de software moderno. Nos repiten una y otra vez que hay que testear, que hay que escribir pruebas unitarias, que hay que validar cada pieza de código… y sí, todo eso es cierto. Pero si algo hemos aprendido con la experiencia, es que no todas las pruebas automatizadas son iguales, ni todas tienen el mismo propósito.
Entonces, si bien podemos hablar de pruebas unitarias, pruebas de integración, pruebas funcionales y demás, el verdadero reto está en saber elegir qué tipo de prueba aplicar en cada caso. Y aquí es donde queremos centrar nuestra conversación de hoy.
No todas las pruebas son iguales
Cuando pensamos en pruebas automatizadas, lo primero que nos viene a la mente son las pruebas unitarias. Son rápidas, verifican pequeñas porciones de código y nos dan confianza en la estabilidad de nuestros módulos. Pero ¿qué pasa cuando nuestro software no es solo una suma de funciones individuales? Ahí es cuando entran en juego las pruebas de integración y las pruebas funcionales, que nos ayudan a validar el comportamiento del sistema en conjunto.
Pero incluso con todo esto, hay un tipo de prueba del que casi no se habla, pero que a mi criterio es el más importante en una estrategia de calidad de software: los smoke tests.
Los smoke tests son pruebas rápidas y ligeras que validan si una aplicación es estable después de una nueva compilación o despliegue. No buscan probar cada detalle, sino verificar que las funciones críticas del sistema están operativas. Son como encender un auto después de repararlo para ver si arranca antes de salir a la carretera.
¿Por qué darle tanta importancia a los smoke tests?
- Son rápidos. En lugar de esperar una batería completa de pruebas unitarias o funcionales, un smoke test puede decirnos en segundos si algo fundamental está roto.
- Detectan problemas graves temprano. Si un smoke test falla, no tiene sentido seguir con pruebas más profundas. Primero hay que corregir lo básico.
- Aseguran estabilidad continua. Integrados en un pipeline de Integración Continua (CI/CD), permiten verificar rápidamente si una nueva versión del software sigue funcionando en sus aspectos esenciales.
- Y, por último pero quizás lo más importante, son muy rápidas de diseñar y preparar
¿Cómo encajan los tipos de pruebas?
Para entender mejor cómo elegir el tipo de prueba adecuado, veamos algunos ejemplos sencillos de cuándo y por qué aplicar cada una:
- Pruebas unitarias: Supongamos que en nuestra aplicación tenemos una clase que convierte precios a distintas monedas. Una prueba unitaria verificaría que si pasamos
10 USD
, la conversión aEUR
se hace correctamente según la tasa actual. Es decir, validamos el comportamiento de una función específica sin depender de otras partes del sistema. En definitiva, estas pruebas prueban las funcionalidades atómicas. - Pruebas funcionales: Imaginemos un flujo de registro de usuario. En este caso, una prueba funcional verificaría que un usuario puede completar el formulario de registro, recibir un correo de confirmación y activar su cuenta con un enlace. Aquí estamos probando el sistema desde la perspectiva del usuario.
- Pruebas de integración: Pensemos en la API de pagos de nuestra aplicación. Una prueba de integración verificaría que, al procesar un pago con Stripe o PayPal, la transacción se registra correctamente en nuestra base de datos y que el usuario recibe un comprobante de pago. Aquí no estamos probando solo una función aislada, sino la interacción entre distintos sistemas o distintas capas del sistema.
- Pruebas de humo: Volviendo al ejemplo de la API de pagos, un smoke test validaría algo más básico: Se invoca a una url y se comprueba que retorne 200 (OK). Este tipo de prueba no entra en los detalles de cada funcionalidad, sino que se enfoca en asegurar que lo esencial del sistema funciona antes de seguir probando más a fondo.
Ejemplo práctico: Un smoke test en Symfony
Para ilustrar la importancia de las pruebas de humo, veamos cómo podríamos implementar una en Symfony utilizando PHPUnit:
namespace App\Tests\Smoke; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; class SmokeTest extends WebTestCase { public function testApplicationIsUp() { $client = static::createClient(); // Verificar si la aplicación responde correctamente $client->request('GET', '/'); $this->assertResponseIsSuccessful(); // Verificar que la página de login carga $client->request('GET', '/login'); $this->assertResponseIsSuccessful(); // Intentar un inicio de sesión básico $client->submitForm('Iniciar sesión', [ 'username' => 'testuser', 'password' => 'password123' ]); $this->assertResponseRedirects('/dashboard'); } }
Este smoke test nos permite verificar rápidamente si la aplicación carga, si la página de login está disponible y si un usuario puede autenticarse correctamente. Si alguna de estas pruebas falla, ya sabemos que algo crítico está roto y no tiene sentido continuar con pruebas más detalladas. Podemos ver como siempre la pregunta es muy sencilla (assert): ¿el response fue exitoso?. No nos ponemos a revisar si lo que ocurrió fue correcto por debajo. Basándonos en esto, podríamos hacer cosas muy sencillas como generalizar lo máximo posible para tener nuestro propio framework de pruebas y usarlo en varios proyectos, reduciendo el tiempo al máximo ya que la realidad de todo proyecto de desarrollo de software es que ni siquiera hay tiempo necesario para documentar y por lo tanto mucho menos para automatizar pruebas.
Haciendo smoke tests reutilizables con una clase base
Lo interesante de los smoke tests es que son tan sencillos que podemos hacerlos aún más reutilizables. Con un poco de pensamiento basado en arquitectura de software, podemos crear una clase base que se encargue de probar si ciertas URLs responden correctamente. Luego, en cada proyecto, solo necesitamos heredar de esta clase y pasarle un array con las rutas a probar.
Aquí tenemos un ejemplo:
namespace App\Tests\Smoke; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; abstract class BaseSmokeTest extends WebTestCase { /** * Método abstracto que las clases hijas deben implementar * para definir las URLs a probar. */ abstract protected function getUrls(): array; public function testPagesAreSuccessful() { $client = static::createClient(); foreach ($this->getUrls() as $url) { $client->request('GET', $url); $this->assertResponseIsSuccessful( "Error: La URL '{$url}' no respondió correctamente." ); } } }
Ya en una prueba de nuestro nuevo proyecto podemos extender la clase base e implementar un solo método para indicar las urls específicas
class MySmokeTest extends BaseSmokeTest { /** * Definimos las URLs a probar. */ protected function getUrls(): array { return [ '/', '/login', '/dashboard', '/profile' ]; } }
Con este enfoque, cada vez que necesitemos probar una nueva lista de URLs en otro proyecto, simplemente heredamos de BaseSmokeTest
, implementamos el método y dejamos que la clase base haga el trabajo por nosotros.
Conclusión: Testear con inteligencia, no con cantidad
Automatizar pruebas no se trata de llenar el código de tests unitarios sin sentido, ni de escribir suites de pruebas funcionales enormes que tarden horas en ejecutarse. Se trata de elegir inteligentemente qué probar y cómo hacerlo.
Y si tuviéramos que priorizar un tipo de prueba, sin duda me inclinaría por los smoke tests. Son simples, efectivos y nos ayudan a detectar problemas críticos de manera inmediata. Con mayor tiempo, tenemos toda una serie de otros tipos que podríamos utilizar para cada caso.
Si te interesa profundizar más en cómo hacer pruebas automatizadas en Symfony, te dejo este video del canal de Neurosimbiosis, donde exploramos las pruebas en Symfony 5 de forma práctica
Descubre más desde Neurosimbiosis
Suscríbete y recibe las últimas entradas en tu correo electrónico.