Diferentes tipos de pruebas responden a preguntas distintas. Una prueba unitaria indica si una función se comporta correctamente de forma aislada. Una prueba de carga indica si el servidor sobrevive a un pico de tráfico. Ninguna responde la pregunta de la otra. La mayoría de las aplicaciones necesita varios tipos trabajando en conjunto.
Este post cubre los diez tipos de pruebas más comunes, para qué sirve cada uno y cómo luce un ejemplo básico en JavaScript o Node.js.
1. Pruebas Unitarias
Definición
Las pruebas unitarias verifican una sola función o método de forma aislada. El objetivo es confirmar que un fragmento de lógica se comporta correctamente para una entrada dada, sin involucrar bases de datos, servicios externos ni otros módulos.
Por qué Importan las Pruebas Unitarias
Son rápidas, por lo que puedes ejecutar miles de ellas en segundos. Cuando una falla, el fallo está localizado: sabes exactamente qué función está rota y bajo qué entrada. También son las pruebas más fáciles de escribir y mantener, ya que no requieren infraestructura.
Ejemplo en Node.js
Usando Jest, un framework de pruebas popular para JavaScript/TypeScript:
// mathOperations.js
function add(a, b) {
return a + b;
}
module.exports = { add };
// mathOperations.test.js
const { add } = require('./mathOperations');
test('adds two numbers correctly', () => {
expect(add(2, 3)).toBe(5);
});
La función add se prueba de forma aislada. Agregarías más pruebas para casos límite: números negativos, entradas de punto flotante y entradas que no son números en absoluto.
2. Pruebas de Integración
Definición
Las pruebas de integración verifican cómo interactúan módulos separados. Van más allá de las funciones individuales para validar el flujo de datos entre componentes: un handler de ruta llamando a un modelo de base de datos, un cliente de API comunicándose con un servicio externo, o dos módulos internos intercambiando datos.
Por qué Importan las Pruebas de Integración
Las unidades individuales pueden comportarse correctamente pero aun así fallar cuando se combinan, porque la interfaz entre ellas es incorrecta. Las pruebas de integración detectan esas incompatibilidades antes de que lleguen a producción.
Ejemplo en Node.js
Considera una aplicación Express.js simple con una llamada a la base de datos:
// userController.js
const express = require('express');
const router = express.Router();
const UserModel = require('./UserModel');
router.get('/users/:id', async (req, res) => {
const user = await UserModel.findById(req.params.id);
if (!user) return res.status(404).send('User not found');
res.json(user);
});
module.exports = router;
// integration.test.js
const request = require('supertest');
const express = require('express');
const userController = require('./userController');
const app = express();
app.use('/', userController);
describe('GET /users/:id', () => {
it('should return a user when valid id is provided', async () => {
// Mock database operations or use an in-memory DB
const response = await request(app).get('/users/123');
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('id', '123');
});
});
Esta prueba confirma que la ruta y el modelo de datos funcionan correctamente juntos. No es una prueba end-to-end completa, pero valida más de lo que haría una prueba unitaria.
3. Pruebas Funcionales
Definición
Las pruebas funcionales verifican que la aplicación hace lo que se supone que debe hacer desde la perspectiva del usuario, comprobando el comportamiento frente a los requisitos especificados. El foco está en qué hace el software, no en cómo lo hace internamente.
Por qué Importan las Pruebas Funcionales
Una función puede ser internamente correcta (todas las pruebas unitarias pasan) y aun así no satisfacer el requisito. Las pruebas funcionales vinculan la implementación con la necesidad del negocio o del usuario, detectando brechas entre la especificación y el comportamiento.
Ejemplo en Node.js
Un flujo de inicio de sesión probado a nivel HTTP:
// login.test.js
const request = require('supertest');
const app = require('./app'); // Your Express.js app with auth routes
describe('User Login Functionality', () => {
it('should return a token if user credentials are valid', async () => {
const response = await request(app)
.post('/auth/login')
.send({ username: 'testUser', password: 'secret123' });
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('token');
});
it('should return 401 if credentials are invalid', async () => {
const response = await request(app)
.post('/auth/login')
.send({ username: 'testUser', password: 'wrongPwd' });
expect(response.status).toBe(401);
});
});
Estas pruebas confirman que la funcionalidad de inicio de sesión funciona con credenciales válidas y rechaza las inválidas, concordando con lo que los requisitos dicen que la funcionalidad debe hacer.
4. Pruebas End-to-End (E2E)
Definición
Las pruebas end-to-end simulan interacciones reales del usuario a través de un navegador, ejercitando el stack completo desde la UI hasta la base de datos y de vuelta. Validan que todo funciona en conjunto en condiciones cercanas a producción.
Por qué Importan las Pruebas E2E
Los cambios en el backend pueden romper flujos del frontend de maneras que ninguna prueba unitaria o de integración detecta. Las pruebas E2E son la red de seguridad para esas regresiones entre capas. Son más lentas y costosas de mantener, por lo que funcionan mejor en los flujos críticos del usuario.
Ejemplo en Node.js
Usando Cypress:
// cypress/e2e/login.spec.js
describe('Login Flow E2E', () => {
it('logs in a user with valid credentials', () => {
cy.visit('http://localhost:3000/login');
cy.get('#username').type('testUser');
cy.get('#password').type('correctPassword');
cy.get('button[type="submit"]').click();
cy.url().should('include', '/dashboard');
cy.contains('Welcome, testUser');
});
});
Esta prueba conduce un navegador real a través del flujo de inicio de sesión y confirma que el usuario llega al dashboard con el mensaje de bienvenida correcto.
5. Pruebas de Rendimiento
Definición
Las pruebas de rendimiento miden cómo se comporta la aplicación bajo carga: tiempos de respuesta, throughput y uso de recursos en distintos niveles de tráfico.
Por qué Importan las Pruebas de Rendimiento
Una aplicación que funciona perfectamente con diez usuarios simultáneos puede colapsar con mil. Las pruebas de rendimiento encuentran ese punto de quiebre antes de que lo encuentren tus usuarios.
Ejemplo en Node.js
Usando Artillery para probar un endpoint de API:
// sampleLoadTest.yml (Artillery syntax)
config:
target: "http://localhost:3000"
phases:
- duration: 30
arrivalRate: 10
scenarios:
- flow:
- get:
url: "/users"
Esta configuración ejecuta 10 solicitudes por segundo contra el endpoint /users durante 30 segundos. Las métricas resultantes muestran percentiles de tiempo de respuesta, tasas de error y throughput.
6. Pruebas de Seguridad
Definición
Las pruebas de seguridad buscan vulnerabilidades: endpoints desprotegidos, inyección SQL, XSS, CSRF y dependencias con vulnerabilidades conocidas.
Por qué Importan las Pruebas de Seguridad
Una sola vulnerabilidad explotada puede exponer datos de usuarios, derribar el servicio o derivar en multas regulatorias. Las pruebas de seguridad hacen explícitas las vulnerabilidades para que puedan corregirse antes de que los atacantes las encuentren.
Ejemplo en Node.js
# Check for known vulnerabilities in the project's dependencies
npm audit
npm audit analiza el árbol de dependencias contra una base de datos de CVEs conocidos y reporta vulnerabilidades por severidad. Para pruebas más exhaustivas, herramientas como OWASP ZAP pueden automatizar el escaneo activo de aplicaciones en ejecución.
7. Pruebas de Aceptación
Definición
Las pruebas de aceptación verifican que el software cumple con los requisitos acordados con stakeholders o clientes. Generalmente se escriben en lenguaje legible por humanos y confirman que la implementación corresponde al caso de uso previsto.
Por qué Importan las Pruebas de Aceptación
La corrección técnica y la aceptación por parte de los stakeholders no son lo mismo. Una funcionalidad puede implementarse exactamente según la especificación y aun así no satisfacer lo que realmente se necesitaba. Las pruebas de aceptación crean una definición compartida de "terminado" entre los desarrolladores y las personas que solicitaron la funcionalidad.
Ejemplo en Node.js
Usando Cucumber con sintaxis Gherkin:
# login.feature
Feature: User Login
In order to access my account
As a registered user
I want to be able to log in using valid credentials
Scenario: Successful login
Given I am on the login page
When I enter valid credentials
Then I should be redirected to the dashboard
And I should see a welcome message
Cada paso se mapea a una definición de paso en JavaScript que ejecuta la acción correspondiente. El escenario es legible por stakeholders no técnicos y ejecutable por el test runner.
8. Pruebas de Carga y Estrés
Definición
Las pruebas de carga verifican el sistema en el pico de tráfico esperado. Las pruebas de estrés van más allá para encontrar dónde falla y cómo se recupera.
Por qué Importan las Pruebas de Carga y Estrés
Saber que tu sistema maneja 500 usuarios simultáneos es útil. Saber qué pasa con 2000, y si se recupera con gracia cuando la carga baja, es más útil para la planificación de capacidad y la preparación ante incidentes.
Ejemplo en Node.js
Un escenario de prueba de estrés con k6:
// stressTest.js
import http from 'k6/http';
import { sleep } from 'k6';
export let options = {
stages: [
{ duration: '2m', target: 100 }, // ramp up from 0 to 100 users over 2 minutes
{ duration: '5m', target: 100 }, // stay at 100 users for 5 minutes
{ duration: '2m', target: 200 }, // ramp up to 200 users
{ duration: '5m', target: 200 }, // stay at 200 users for 5 minutes
{ duration: '1m', target: 0 }, // ramp down
],
};
export default function() {
http.get('http://localhost:3000/api/resource');
sleep(1);
}
Este script incrementa gradualmente hasta 200 usuarios simultáneos, mantiene esa carga y luego la reduce. Las métricas en cada etapa muestran cómo se degrada el rendimiento bajo carga sostenida.
9. Pruebas de Regresión
Definición
Las pruebas de regresión vuelven a ejecutar las pruebas existentes tras cambios en el código para confirmar que las funcionalidades que funcionaban anteriormente siguen funcionando.
Por qué Importan las Pruebas de Regresión
Todo cambio de código es una regresión potencial. Las refactorizaciones, las actualizaciones de dependencias y las nuevas funcionalidades conllevan el riesgo de romper algo que funcionaba antes. Ejecutar el conjunto completo de pruebas en cada commit hace explícito ese riesgo.
Ejemplo en Node.js
Una suite de regresión es simplemente las pruebas unitarias, de integración y E2E existentes ejecutadas juntas:
// All test files combined under a "regression" script
{
"scripts": {
"test:regression": "jest --config=jest.config.js --runInBand"
}
}
Un solo comando vuelve a ejecutar todo. Cualquier fallo es una regresión que debe investigarse antes de hacer el merge.
10. Pruebas Exploratorias
Definición
Las pruebas exploratorias son pruebas manuales sin guion en las que un desarrollador o tester explora activamente la aplicación para encontrar comportamientos inesperados, problemas de usabilidad o casos límite que las pruebas automatizadas no anticiparon.
Por qué Importan las Pruebas Exploratorias
Las pruebas automatizadas solo encuentran bugs que alguien pensó en probar. Un tester que sondea activamente el sistema con curiosidad y conocimiento del dominio encuentra una clase distinta de problemas: flujos de interacción extraños, estados de error confusos y comportamientos que son técnicamente correctos pero prácticamente incorrectos.
Ejemplo en la Práctica
Una sesión de pruebas exploratorias puede verse así: iniciar sesión como usuarios con distintos niveles de permisos, cancelar operaciones a mitad de camino, refrescar la página en pasos críticos, enviar formularios con entradas inusuales. Nada de esto está guionizado. El tester registra lo que encuentra para triaje y seguimiento.
Las pruebas exploratorias funcionan mejor como complemento a la automatización, no como sustituto. Automatiza los caminos conocidos y explora los desconocidos.
Construyendo un Pipeline de Pruebas Integral
Reunir todo esto en un pipeline funcional requiere algunas decisiones. Integra las pruebas en el CI para que cada commit dispare una ejecución. Las pruebas que fallan bloquean los merges. Usa nombres de prueba descriptivos para que los fallos sean autoexplicativos sin necesidad de adentrarse en el código. Mantén los archivos de prueba en el mismo repositorio que el código que prueban. Monitorea la cobertura con Istanbul o una herramienta similar para identificar rutas no probadas, especialmente en el código de la capa de servicio.
El objetivo es una suite en la que cada tipo de prueba cubra los modos de fallo que los demás pasan por alto: pruebas unitarias para bugs de lógica, pruebas de integración para incompatibilidades de interfaz, pruebas E2E para regresiones entre capas, y pruebas de rendimiento y seguridad para riesgos operativos. Cada capa es rápida donde puede serlo, y exhaustiva donde la velocidad no importa tanto.