# Testing y QA de Software: Mejores Prácticas 2026
Hay dos tipos de empresas: las que invierten en testing y las que descubren sus bugs cuando los reporta un cliente furioso en producción. La segunda opción es significativamente más cara — y no solo en dinero. Un bug en producción puede costar entre 10x y 100x más que encontrarlo durante el desarrollo, según datos del NIST y múltiples estudios de la industria.
En 2026, con la complejidad creciente de los sistemas empresariales — microservicios, integraciones con APIs de terceros, múltiples frontends, regulaciones de datos — el testing ya no es algo "que hacemos si nos queda tiempo". Es una disciplina de ingeniería que, hecha correctamente, es la diferencia entre software que genera confianza y software que genera tickets de soporte.
Tabla de Contenidos
- QA vs Testing: la diferencia que importa
- La pirámide de testing en 2026
- Unit testing: la base de todo
- Integration testing: donde los módulos se encuentran
- End-to-End testing: el flujo completo
- Performance testing: bajo presión
- Security testing: antes de que alguien más lo haga
- Automatización de pruebas: estrategia práctica
- Testing en CI/CD pipelines
- Herramientas recomendadas 2026
- Métricas de calidad que importan
- Cuánto invertir en testing
- Errores comunes en QA
- Preguntas frecuentes
---
QA vs Testing: la diferencia que importa
Testing
Testing es la actividad de ejecutar pruebas para encontrar defectos. Es una parte de QA, no todo QA.
- Ejecutar test cases
- Reportar bugs
- Verificar fixes
- Automatizar pruebas
QA (Quality Assurance)
QA es el proceso completo de asegurar calidad en todo el ciclo de desarrollo:
- Revisión de requerimientos (¿están claros y completos?)
- Code reviews (¿el código sigue estándares?)
- Procesos de desarrollo (¿hay CI/CD, branching strategy?)
- Testing (todas las capas)
- Monitoreo post-deploy (¿el sistema se comporta como se esperaba?)
- Mejora continua (¿aprendemos de los bugs?)
Por qué la distinción importa
Muchas empresas mexicanas contratan a un "QA" y le piden que haga testing manual antes de cada release. Eso no es QA — es un tester. QA real implica que la calidad se construye en cada paso del proceso, no se inspecciona al final.
Analogía: QA es como construir una casa con planos, materiales de calidad y supervisión en cada etapa. Testing es como revisar la casa terminada buscando grietas. Ambos son necesarios, pero solo hacer testing es como construir a ciegas y esperar que salga bien.
---
La pirámide de testing en 2026
La pirámide de testing sigue siendo el modelo más útil para estructurar tu estrategia:
```
╱╲
╱ ╲
╱ E2E╲ ← Pocos, lentos, costosos
╱______╲
╱ ╲
╱Integration╲ ← Moderados
╱____________╲
╱ ╲
╱ Unit Tests ╲ ← Muchos, rápidos, baratos
╱__________________╲
```
Distribución recomendada
| Tipo | % del total | Velocidad | Costo de mantener |
|---|
| Unit | 60–70% | Milisegundos | Bajo |
|---|---|---|---|
| Integration | 20–30% | Segundos | Medio |
| E2E | 5–10% | Minutos | Alto |
La anti-pirámide (el problema)
Muchas empresas tienen esto:
```
╱╲
╱ ╲
╱Unit╲ ← Casi ninguno
╱______╲
╱ ╲
╱Integration╲ ← Pocos
╱____________╲
╱ ╲
╱ Manual E2E ╲ ← Todo se prueba manualmente
╱________________╲
```
Esto resulta en ciclos de testing largos, releases lentos y bugs que se descubren tarde.
---
Unit testing: la base de todo
Qué son
Tests que verifican una unidad aislada de código (función, método, clase) sin dependencias externas (bases de datos, APIs, filesystem).
Ejemplo práctico
```typescript
// pricing.service.ts
export class PricingService {
calculateDiscount(
subtotal: number,
customerTier: 'standard' | 'premium' | 'enterprise'
): number {
if (subtotal <= 0) throw new Error('Subtotal must be positive');
const discountRates: Record
standard: 0,
premium: 0.10,
enterprise: 0.20,
};
const rate = discountRates[customerTier] ?? 0;
return Math.round(subtotal rate 100) / 100;
}
calculateTax(subtotal: number, region: 'MX' | 'US'): number {
const taxRates: Record
MX: 0.16, // IVA
US: 0.0875, // Average US sales tax
};
return Math.round(subtotal (taxRates[region] ?? 0) 100) / 100;
}
calculateTotal(
subtotal: number,
customerTier: 'standard' | 'premium' | 'enterprise',
region: 'MX' | 'US'
): { subtotal: number; discount: number; tax: number; total: number } {
const discount = this.calculateDiscount(subtotal, customerTier);
const afterDiscount = subtotal - discount;
const tax = this.calculateTax(afterDiscount, region);
return {
subtotal,
discount,
tax,
total: Math.round((afterDiscount + tax) * 100) / 100,
};
}
}
```
```typescript
// pricing.service.test.ts
import { PricingService } from './pricing.service';
describe('PricingService', () => {
let service: PricingService;
beforeEach(() => {
service = new PricingService();
});
describe('calculateDiscount', () => {
it('returns 0 for standard customers', () => {
expect(service.calculateDiscount(1000, 'standard')).toBe(0);
});
it('returns 10% for premium customers', () => {
expect(service.calculateDiscount(1000, 'premium')).toBe(100);
});
it('returns 20% for enterprise customers', () => {
expect(service.calculateDiscount(1000, 'enterprise')).toBe(200);
});
it('handles decimal amounts correctly', () => {
expect(service.calculateDiscount(99.99, 'premium')).toBe(10);
});
it('throws error for negative subtotal', () => {
expect(() => service.calculateDiscount(-100, 'premium'))
.toThrow('Subtotal must be positive');
});
it('throws error for zero subtotal', () => {
expect(() => service.calculateDiscount(0, 'premium'))
.toThrow('Subtotal must be positive');
});
});
describe('calculateTax', () => {
it('calculates 16% IVA for México', () => {
expect(service.calculateTax(1000, 'MX')).toBe(160);
});
it('calculates 8.75% for US', () => {
expect(service.calculateTax(1000, 'US')).toBe(87.5);
});
});
describe('calculateTotal', () => {
it('calculates correct total for enterprise MX customer', () => {
const result = service.calculateTotal(10000, 'enterprise', 'MX');
expect(result.subtotal).toBe(10000);
expect(result.discount).toBe(2000); // 20%
expect(result.tax).toBe(1280); // 16% of 8000
expect(result.total).toBe(9280); // 8000 + 1280
});
it('calculates correct total for standard MX customer', () => {
const result = service.calculateTotal(5000, 'standard', 'MX');
expect(result.discount).toBe(0);
expect(result.tax).toBe(800); // 16% of 5000
expect(result.total).toBe(5800);
});
});
});
```
Mejores prácticas de unit testing
- Nombrado descriptivo: `it('returns 10% discount for premium customers')` — no `it('test1')`
- AAA pattern: Arrange (setup), Act (ejecutar), Assert (verificar)
- Un assert por concepto: Cada test verifica una cosa
- Tests independientes: Ningún test depende de otro
- Mock de dependencias: Usa mocks/stubs para bases de datos, APIs, etc.
- Edge cases: Testa valores límite, nulos, vacíos, negativos
- Cobertura mínima 80%: Pero no busques 100% — tiene rendimientos decrecientes
Cuándo NO hacer unit test
- Código trivial (getters/setters simples)
- Código generado automáticamente
- Código de UI puro (mejor con integration tests)
- Prototipos y MVPs de una semana de vida
---
¿Tu equipo necesita mejorar sus prácticas de testing? Nuestro equipo de desarrollo implementa QA profesional desde el día uno en cada proyecto.
---
Integration testing: donde los módulos se encuentran
Qué son
Tests que verifican la interacción entre dos o más componentes: tu código con la base de datos, tu API con un servicio externo, tu frontend con tu backend.
Tipos de integration tests
1. Base de datos:
Verificar que tus queries, migrations y ORM funcionan correctamente.
```typescript
// order.repository.integration.test.ts
import { OrderRepository } from './order.repository';
import { TestDatabase } from '../test-utils/database';
describe('OrderRepository', () => {
let db: TestDatabase;
let repo: OrderRepository;
beforeAll(async () => {
db = await TestDatabase.create();
repo = new OrderRepository(db.connection);
});
afterAll(async () => {
await db.destroy();
});
beforeEach(async () => {
await db.clean(); // Limpiar tablas entre tests
});
it('creates an order and retrieves it', async () => {
const order = await repo.create({
customerId: 'cust-001',
items: [
{ productId: 'prod-001', quantity: 2, price: 500 },
{ productId: 'prod-002', quantity: 1, price: 1200 },
],
});
const retrieved = await repo.findById(order.id);
expect(retrieved).toBeDefined();
expect(retrieved!.customerId).toBe('cust-001');
expect(retrieved!.items).toHaveLength(2);
expect(retrieved!.total).toBe(2200);
});
it('filters orders by date range', async () => {
// Arrange: create orders with different dates
await repo.create({
customerId: 'cust-001',
createdAt: new Date('2026-01-15'),
items: [{ productId: 'p1', quantity: 1, price: 100 }],
});
await repo.create({
customerId: 'cust-001',
createdAt: new Date('2026-03-15'),
items: [{ productId: 'p2', quantity: 1, price: 200 }],
});
// Act
const orders = await repo.findByDateRange(
new Date('2026-03-01'),
new Date('2026-03-31')
);
// Assert
expect(orders).toHaveLength(1);
expect(orders[0].total).toBe(200);
});
});
```
2. API (HTTP):
Verificar que tus endpoints responden correctamente.
```typescript
// orders.api.integration.test.ts
import request from 'supertest';
import { app } from '../app';
import { TestDatabase } from '../test-utils/database';
describe('Orders API', () => {
let db: TestDatabase;
beforeAll(async () => {
db = await TestDatabase.create();
});
afterAll(async () => {
await db.destroy();
});
describe('POST /api/orders', () => {
it('creates an order successfully', async () => {
const response = await request(app)
.post('/api/orders')
.set('Authorization', 'Bearer test-token')
.send({
customerId: 'cust-001',
items: [
{ productId: 'prod-001', quantity: 2, price: 500 },
],
});
expect(response.status).toBe(201);
expect(response.body.id).toBeDefined();
expect(response.body.total).toBe(1000);
});
it('returns 400 for empty items', async () => {
const response = await request(app)
.post('/api/orders')
.set('Authorization', 'Bearer test-token')
.send({ customerId: 'cust-001', items: [] });
expect(response.status).toBe(400);
expect(response.body.error).toContain('items');
});
it('returns 401 without auth token', async () => {
const response = await request(app)
.post('/api/orders')
.send({ customerId: 'cust-001', items: [] });
expect(response.status).toBe(401);
});
});
});
```
3. Servicios externos (con contract testing):
Cuando dependes de APIs de terceros, usa contract tests para verificar que tu código maneja las respuestas correctamente sin llamar al servicio real:
```typescript
// payment-gateway.contract.test.ts
import nock from 'nock';
import { PaymentGateway } from './payment-gateway';
describe('PaymentGateway', () => {
const gateway = new PaymentGateway('https://api.payment.com');
afterEach(() => {
nock.cleanAll();
});
it('processes payment successfully', async () => {
nock('https://api.payment.com')
.post('/v1/charges')
.reply(200, {
id: 'ch_123',
status: 'succeeded',
amount: 10000,
currency: 'MXN',
});
const result = await gateway.charge({
amount: 10000,
currency: 'MXN',
cardToken: 'tok_test',
});
expect(result.status).toBe('succeeded');
expect(result.chargeId).toBe('ch_123');
});
it('handles payment decline gracefully', async () => {
nock('https://api.payment.com')
.post('/v1/charges')
.reply(402, { error: { code: 'card_declined' } });
const result = await gateway.charge({
amount: 10000,
currency: 'MXN',
cardToken: 'tok_declined',
});
expect(result.status).toBe('declined');
});
});
```
Testcontainers: bases de datos reales en tests
En 2026, Testcontainers es el estándar para integration tests con bases de datos:
```typescript
import { PostgreSqlContainer } from '@testcontainers/postgresql';
let container;
beforeAll(async () => {
container = await new PostgreSqlContainer()
.withDatabase('testdb')
.start();
// Usar container.getConnectionUri() para conectar
}, 30000); // Puede tardar 10-30s en arrancar
afterAll(async () => {
await container.stop();
});
```
Levanta una base de datos PostgreSQL real en un container Docker para cada suite de tests. Lento, pero prueba el código contra la base de datos real, no un mock.
---
End-to-End testing: el flujo completo
Qué son
Tests que simulan el comportamiento de un usuario real interactuando con el sistema completo: navegador → frontend → backend → base de datos.
Playwright: el estándar 2026
Playwright (de Microsoft) ha superado a Cypress como la herramienta preferida para E2E testing:
```typescript
// orders-flow.e2e.test.ts
import { test, expect } from '@playwright/test';
test.describe('Order Creation Flow', () => {
test.beforeEach(async ({ page }) => {
// Login
await page.goto('/login');
await page.fill('[data-testid="email"]', 'test@empresa.com');
await page.fill('[data-testid="password"]', 'TestPass123');
await page.click('[data-testid="login-button"]');
await page.waitForURL('/dashboard');
});
test('creates an order successfully', async ({ page }) => {
// Navigate to orders
await page.click('[data-testid="nav-orders"]');
await page.click('[data-testid="new-order-button"]');
// Select customer
await page.fill('[data-testid="customer-search"]', 'Acme Corp');
await page.click('[data-testid="customer-option-acme"]');
// Add product
await page.fill('[data-testid="product-search"]', 'Widget Pro');
await page.click('[data-testid="product-option-widget"]');
await page.fill('[data-testid="quantity"]', '5');
// Verify totals
await expect(page.locator('[data-testid="subtotal"]'))
.toHaveText('$2,500.00');
await expect(page.locator('[data-testid="tax"]'))
.toHaveText('$400.00');
await expect(page.locator('[data-testid="total"]'))
.toHaveText('$2,900.00');
// Submit order
await page.click('[data-testid="submit-order"]');
// Verify confirmation
await expect(page.locator('[data-testid="order-confirmation"]'))
.toBeVisible();
await expect(page.locator('[data-testid="order-status"]'))
.toHaveText('Confirmado');
});
test('shows validation errors for incomplete order', async ({ page }) => {
await page.click('[data-testid="nav-orders"]');
await page.click('[data-testid="new-order-button"]');
// Try to submit without selecting customer
await page.click('[data-testid="submit-order"]');
await expect(page.locator('[data-testid="error-customer"]'))
.toHaveText('Selecciona un cliente');
});
});
```
Mejores prácticas para E2E
- Solo flujos críticos: Login, checkout, registro, flujos de pago
- Data-testid attributes: No depender de clases CSS o texto que cambia
- Setup/teardown de datos: Cada test crea sus datos y los limpia
- No más de 20–30 tests E2E: Más que eso es insostenible
- Parallelizar: Playwright ejecuta tests en paralelo automáticamente
- Visual regression: Screenshots para detectar cambios visuales inesperados
- Retry logic: Los E2E tests son flaky — configura 2 retries antes de marcar como fallido
Visual regression testing
```typescript
// visual-regression.test.ts
import { test, expect } from '@playwright/test';
test('homepage matches visual snapshot', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
await expect(page).toHaveScreenshot('homepage.png', {
maxDiffPixelRatio: 0.01, // 1% de diferencia permitida
});
});
test('order form matches visual snapshot', async ({ page }) => {
await page.goto('/orders/new');
await expect(page).toHaveScreenshot('order-form.png');
});
```
---
Performance testing: bajo presión
Por qué importa
Tu aplicación funciona perfectamente con 10 usuarios. ¿Qué pasa con 1,000? ¿O con 10,000 en Black Friday? Performance testing responde esas preguntas antes de que sea demasiado tarde.
Tipos de performance testing
| Tipo | Objetivo | Ejemplo |
|---|
| Load testing | Comportamiento bajo carga esperada | 500 usuarios simultáneos |
|---|---|---|
| Stress testing | Encontrar el punto de quiebre | Aumentar usuarios hasta que falle |
| Spike testing | Respuesta a picos repentinos | De 100 a 5,000 usuarios en 30 segundos |
| Endurance testing | Estabilidad a largo plazo | 500 usuarios durante 8 horas |
| Scalability testing | Comportamiento al escalar | ¿Duplicar servidores duplica la capacidad? |
k6: performance testing moderno
k6 (de Grafana Labs) es la herramienta recomendada en 2026:
```javascript
// load-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 100 }, // Ramp up a 100 usuarios
{ duration: '5m', target: 100 }, // Mantener 100 usuarios
{ duration: '2m', target: 500 }, // Ramp up a 500
{ duration: '5m', target: 500 }, // Mantener 500
{ duration: '2m', target: 0 }, // Ramp down
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% de requests < 500ms
http_req_failed: ['rate<0.01'], // Menos de 1% de errores
},
};
export default function () {
// Simular flujo de usuario
const loginRes = http.post('https://api.miapp.com/auth/login', JSON.stringify({
email: `user${__VU}@test.com`,
password: 'TestPass123',
}), { headers: { 'Content-Type': 'application/json' } });
check(loginRes, {
'login exitoso': (r) => r.status === 200,
'tiene token': (r) => r.json('token') !== undefined,
});
const token = loginRes.json('token');
sleep(1);
// Consultar pedidos
const ordersRes = http.get('https://api.miapp.com/api/orders', {
headers: { Authorization: `Bearer ${token}` },
});
check(ordersRes, {
'orders status 200': (r) => r.status === 200,
'responde en menos de 500ms': (r) => r.timings.duration < 500,
});
sleep(2);
}
```
Métricas de performance clave
| Métrica | Target típico | Crítico |
|---|
| Response time p50 | < 200ms | > 500ms |
|---|---|---|
| Response time p95 | < 500ms | > 2s |
| Response time p99 | < 1s | > 5s |
| Error rate | < 0.1% | > 1% |
| Throughput | Depende | Caída > 20% |
| CPU utilization | < 70% | > 90% |
| Memory utilization | < 80% | > 95% |
---
¿Preocupado por el rendimiento de tu aplicación bajo carga? Nuestro equipo ejecuta performance testing profesional y te entrega un reporte con recomendaciones concretas.
---
Security testing: antes de que alguien más lo haga
OWASP Top 10 en 2026
Las vulnerabilidades más comunes siguen siendo sorprendentemente predecibles:
- Broken Access Control: Usuarios accediendo a datos que no les corresponden
- Cryptographic Failures: Datos sensibles sin cifrar
- Injection: SQL injection, NoSQL injection, command injection
- Insecure Design: Fallos de diseño, no de implementación
- Security Misconfiguration: Headers faltantes, puertos abiertos, debug en producción
- Vulnerable Components: Librerías con vulnerabilidades conocidas
- Authentication Failures: Contraseñas débiles, sesiones mal gestionadas
- Data Integrity Failures: Actualizaciones sin verificar, deserialización insegura
- Logging Failures: No registrar eventos de seguridad
- SSRF: Server-Side Request Forgery
Tipos de security testing
SAST (Static Application Security Testing):
- Analiza el código fuente sin ejecutarlo
- Encuentra vulnerabilidades como SQL injection, XSS, hardcoded secrets
- Herramientas: SonarQube, Semgrep, CodeQL
- Integrable en CI/CD (obligatorio)
DAST (Dynamic Application Security Testing):
- Prueba la aplicación en ejecución desde fuera
- Simula ataques como un hacker lo haría
- Herramientas: OWASP ZAP, Burp Suite, Nuclei
- Ejecutar en ambiente de staging
SCA (Software Composition Analysis):
- Escanea dependencias (npm, pip, maven) buscando vulnerabilidades conocidas
- Herramientas: Snyk, Dependabot, npm audit, Trivy
- Integrable en CI/CD (obligatorio)
Penetration Testing:
- Hackers éticos intentan comprometer tu sistema
- Manual + automatizado
- Recomendado anualmente o antes de releases mayores
- Costo: $5,000–$25,000 USD dependiendo del alcance
Ejemplo: SAST con Semgrep en CI
```yaml
# En tu pipeline de CI
- name: Security Scan (SAST)
semgrep --config=auto \
--config=p/owasp-top-ten \
--config=p/javascript \
--error \
--json \
--output=semgrep-results.json
```
Headers de seguridad obligatorios
```typescript
// middleware/security-headers.ts
export function securityHeaders(req, res, next) {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '0'); // Deprecated, usar CSP
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self'");
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
next();
}
```
---
Automatización de pruebas: estrategia práctica {#automatización}
Qué automatizar y qué no
Automatizar:
- Tests unitarios (siempre)
- Tests de integración de API
- Smoke tests (verificación básica post-deploy)
- Tests de regresión (funcionalidad existente que no debe romperse)
- Security scans
- Performance baselines
No automatizar (o automatizar después):
- Testing exploratorio (requiere intuición humana)
- Testing de usabilidad (requiere usuarios reales)
- Tests de funcionalidad nueva que cambia frecuentemente
- Edge cases extremadamente raros
- Tests que son más caros de mantener que de ejecutar manualmente
El costo oculto de la automatización
Cada test automatizado tiene un costo de mantenimiento. Un test E2E que se rompe con cada cambio de UI no ahorra tiempo — lo consume.
Regla del pulgar: Si un test se rompe más de 3 veces al mes sin que haya un bug real, ese test necesita ser reescrito o eliminado.
Test data management
Uno de los mayores retos de la automatización:
Opciones:
- Fixtures/seeds: Datos predefinidos que se cargan antes de cada test
- Factories: Generación dinámica de datos (faker.js, factory-bot)
- Snapshots de producción anonimizados: Datos reales sin información personal
- API-driven setup: Crear datos vía API al inicio de cada test
Recomendación: Factories para unit e integration tests, API-driven setup para E2E tests.
```typescript
// factories/order.factory.ts
import { faker } from '@faker-js/faker';
export function createOrderData(overrides = {}) {
return {
customerId: faker.string.uuid(),
items: [
{
productId: faker.string.uuid(),
quantity: faker.number.int({ min: 1, max: 10 }),
price: faker.number.float({ min: 100, max: 10000, fractionDigits: 2 }),
},
],
...overrides,
};
}
```
---
Testing en CI/CD pipelines
Pipeline de testing recomendado
```
┌──────────────────────────────────────────────────────────┐
│ CI Pipeline │
├──────────┬──────────┬──────────┬──────────┬─────────────┤
│ Lint │ Unit │ Integ. │ Security │ Build │
│ (30s) │ (2min) │ (5min) │ (3min) │ (2min) │
│ Parallel │ Parallel │ Parallel │ Parallel │ Sequential │
└──────────┴──────────┴──────────┴──────────┴─────────────┘
│
┌──────┴──────┐
│ CD to Dev │ (automático)
└──────┬──────┘
│
┌──────┴──────┐
│ Smoke E2E │ (5 tests críticos)
└──────┬──────┘
│
┌──────┴──────┐
│ CD to Stage │ (automático)
└──────┬──────┘
│
┌──────┴──────┐
│ Full E2E │ (20-30 tests)
│ + Perf │
└──────┬──────┘
│
┌──────┴──────┐
│ CD to Prod │ (manual approval)
└─────────────┘
```
Quality gates
Criterios que deben cumplirse para avanzar al siguiente stage:
| Gate | Criterio |
|---|
| CI verde | 0 tests fallidos |
|---|---|
| Cobertura | ≥ 80% (no baja vs main) |
| Security | 0 vulnerabilidades críticas o altas |
| Performance | p95 < 500ms |
| E2E | 100% de smoke tests verdes |
| Manual | Aprobación de QA lead o PM |
Caching para tests rápidos
```yaml
# GitHub Actions - cache de dependencias
- name: Cache node_modules
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
# Paralelizar tests
- name: Run tests
strategy:
matrix:
shard: ['1/4', '2/4', '3/4', '4/4']
```
---
Herramientas recomendadas 2026
Stack completo de testing
| Categoría | Herramienta | Por qué |
|---|
| Unit testing | Jest / Vitest | Rápido, buen DX, estándar de la industria |
|---|---|---|
| Integration testing | Supertest + Testcontainers | APIs + DB real en containers |
| E2E testing | Playwright | Multi-browser, rápido, gran API |
| Performance testing | k6 | Moderno, scriptable, integra con Grafana |
| SAST | Semgrep | Open source, fácil de configurar |
| SCA | Snyk / Dependabot | Scan de dependencias automático |
| DAST | OWASP ZAP | Open source, completo |
| Visual regression | Playwright (built-in) | Screenshots comparativos |
| API testing | Postman / Bruno | Collections compartibles |
| Mocking | MSW (Mock Service Worker) | Intercepción de network moderna |
| Coverage | Istanbul / c8 | Integrado con Jest/Vitest |
| CI/CD | GitHub Actions | Integración natural |
Costos de herramientas
| Herramienta | Costo |
|---|
| Jest, Vitest, Playwright, k6, Semgrep, ZAP | Gratis (open source) |
|---|---|
| Snyk | Free tier (200 tests/mes), Pro $50+/mes |
| Postman | Free tier, Team $12/user/mes |
| SonarQube | Community Edition gratis, Developer $150+/año |
| GitHub Actions | 2,000 min/mes gratis, luego $0.008/min |
---
Métricas de calidad que importan
Métricas de testing
| Métrica | Target | Cómo medir |
|---|
| Code coverage | ≥ 80% | Istanbul/c8 en CI |
|---|---|---|
| Test pass rate | ≥ 99% | CI dashboard |
| Flaky test rate | < 2% | Tracking de retries |
| Test execution time | CI < 10 min | CI timing |
| Bug escape rate | < 5% | Bugs en prod vs total bugs |
Métricas de calidad de producto
| Métrica | Target | Cómo medir |
|---|
| Defect density | < 1 bug/KLOC | Bug tracker + LOC count |
|---|---|---|
| MTTR (Mean Time To Repair) | < 4 horas | Incident management |
| Customer-reported bugs | Tendencia a la baja | Support tickets |
| Post-release defects | < 3 por release | Bug tracker |
| Technical debt ratio | < 5% | SonarQube |
Dashboard de calidad recomendado
Configura un dashboard visible para todo el equipo con:
- Test results del último pipeline
- Code coverage trend (últimos 30 días)
- Open bugs por severidad
- Bug escape rate por sprint
- Performance baselines
---
Cuánto invertir en testing
El ratio recomendado
La industria recomienda que el testing represente entre el 15% y 35% del esfuerzo total de desarrollo.
| Tipo de proyecto | % de testing | Justificación |
|---|
| MVP / Prototipo | 10–15% | Velocidad sobre calidad |
|---|---|---|
| Aplicación empresarial | 20–25% | Balance costo-calidad |
| Fintech / Healthcare | 30–40% | Regulación, riesgo alto |
| Infraestructura crítica | 40–50% | Falla = consecuencias graves |
En términos de equipo
Para un equipo de 8 desarrolladores en un proyecto empresarial:
- 1–2 personas enfocadas en QA (automation + estrategia)
- Todos los desarrolladores escriben unit tests
- QA lead define estrategia y mantiene E2E tests
- Performance y security testing: trimestral o pre-release
Costo de NO invertir
Un estudio de IBM calcula que el costo de corregir un bug varía enormemente según la fase:
| Fase de detección | Costo relativo |
|---|
| Requerimientos | 1x |
|---|---|
| Diseño | 5x |
| Desarrollo | 10x |
| Testing | 20x |
| Producción | 100x |
Un bug que costaría $500 MXN corregir durante el desarrollo cuesta $50,000 MXN cuando lo reporta un cliente en producción (incluyendo el costo de soporte, hotfix, re-testing, deploy urgente y daño a la reputación).
---
Errores comunes en QA
Error 1: "Testing es responsabilidad del QA"
Problema: Los desarrolladores escriben código y "lo pasan a QA" para que lo pruebe.
Solución: Los desarrolladores escriben unit tests e integration tests. QA se enfoca en E2E, exploratory testing y estrategia de calidad.
Error 2: 100% de code coverage como objetivo
Problema: El equipo persigue 100% de cobertura, escribiendo tests inútiles solo para subir el número.
Solución: Apunta a 80% con tests significativos. Un test que verifica un getter trivial no aporta valor.
Error 3: Tests frágiles que siempre se rompen
Problema: Los E2E tests se rompen con cada cambio de UI, y el equipo los ignora o los marca como "skip".
Solución: Usa `data-testid` en lugar de selectores CSS. Reduce la cantidad de E2E tests. Mantén solo los que prueban flujos críticos.
Error 4: No testear el camino triste
Problema: Solo se testan los happy paths. Nadie prueba qué pasa cuando la API devuelve 500, cuando el usuario mete datos inválidos o cuando la conexión se pierde.
Solución: Por cada happy path test, escribe al menos 2–3 tests de error handling.
Error 5: Testing manual antes de cada release
Problema: Un ciclo manual de testing de 3–5 días antes de cada release. Lento, inconsistente y caro.
Solución: Automatiza las pruebas de regresión. El testing manual debería enfocarse en testing exploratorio de funcionalidad nueva.
Error 6: No tener ambiente de staging
Problema: Los tests corren contra un ambiente "que se parece" a producción pero tiene datos diferentes, configuración diferente y escala diferente.
Solución: Staging debe ser un espejo de producción (misma configuración, datos anonimizados, misma escala reducida). Los E2E tests corren aquí.
---
Preguntas frecuentes
¿Cuántos tests necesita mi aplicación?
No hay un número mágico. Una aplicación bien testeada tiene: unit tests para toda la lógica de negocio (80%+ coverage), integration tests para APIs y base de datos, y 15–30 E2E tests para flujos críticos. Para una app de tamaño medio, esto puede ser 500–2000 tests en total.
¿Jest o Vitest en 2026?
Vitest es más rápido y tiene mejor integración con proyectos Vite/Vue. Jest es más maduro y tiene el mayor ecosistema. Para proyectos nuevos con Vite, usa Vitest. Para todo lo demás, Jest sigue siendo excelente.
¿Cypress o Playwright?
Playwright. En 2026, Playwright tiene mejor soporte multi-browser, es más rápido, soporta múltiples tabs/ventanas, y tiene mejor API para testing moderno. Cypress sigue siendo bueno pero su modelo single-tab es limitante.
¿Debo hacer testing de APIs con Postman o con código?
Ambos tienen su lugar. Postman/Bruno para exploración manual y documentación de APIs. Tests en código (Supertest, Playwright API testing) para automatización en CI/CD. Los tests automatizados de API deben estar en código, no en Postman.
¿Cuándo vale la pena hacer performance testing?
Siempre que tu aplicación tenga más de 100 usuarios concurrentes, maneje transacciones financieras, o tenga SLAs de disponibilidad. Para aplicaciones internas con 10 usuarios, probablemente no necesitas performance testing formal.
¿Es necesario contratar penetration testing externo?
Si tu aplicación maneja datos sensibles (personales, financieros, médicos), sí. Anualmente o antes de releases mayores. Un pentest profesional cuesta $5,000–$25,000 USD y puede encontrar vulnerabilidades que ninguna herramienta automatizada detecta.
¿Cómo manejo los tests flaky?
Identifica los tests flaky (los que a veces pasan y a veces fallan sin cambios de código). Opciones: (1) agregar retry logic, (2) agregar waits más inteligentes, (3) aislar mejor los datos del test, (4) eliminar el test si no aporta valor suficiente.
¿Debo testear código de terceros (librerías)?
No. No testes el framework, la librería o la base de datos. Testa TU código que usa esas herramientas. Si haces un wrapper sobre una librería, testa el wrapper.
---
Conclusión
El testing no es un gasto — es una inversión que se paga sola. Cada bug encontrado en testing es un incidente evitado en producción, un cliente que no se frustra, un equipo de soporte que no trabaja en fin de semana.
La clave está en la estrategia: muchos unit tests rápidos, integration tests para las interfaces, pocos E2E tests para flujos críticos, y security y performance testing antes de cada release significativo. Automatiza lo que se repite, pero no automatices todo — el testing exploratorio humano sigue siendo irremplazable para encontrar bugs que nadie esperaba.
En iTechDev, cada proyecto de desarrollo incluye QA profesional desde el primer sprint. No como un add-on o una fase al final — como una práctica integrada en cada línea de código. Porque entregar software sin testing adecuado no es entregar rápido, es entregar deuda.
Conoce nuestros servicios de desarrollo de software a la medida →
