Testing y QA automatizado desarrollo web freelance Valencia: Guía completa Jest, Playwright, Cypress 2025
Testing y QA automatizado desarrollo web freelance Valencia: Guía completa Jest, Playwright, Cypress 2025
Introducción: Testing no es opcional
Como desarrollador freelance que ha trabajado en proyectos críticos, te digo una verdad dolorosa: el código sin tests está roto por defecto.
He visto proyectos que se caen en producción, bugs que cuestan miles de euros en pérdidas, y clientes que pierden confianza. Todo porque no invirtieron tiempo en testing.
En este artículo te doy mi stack completo de testing que uso en todos mis proyectos: desde unit tests hasta monitoring en producción.
Los 6 niveles de testing en desarrollo web
1. Testing unitario: La base de todo
¿Qué testear?
Funciones puras, utilidades, lógica de negocio.
// utils/calculatePrice.test.ts
import { calculatePrice } from './calculatePrice';
describe('calculatePrice', () => {
it('should calculate price with discount', () => {
const result = calculatePrice(100, 10); // 10% discount
expect(result).toBe(90);
});
it('should handle zero discount', () => {
const result = calculatePrice(100, 0);
expect(result).toBe(100);
});
it('should handle 100% discount', () => {
const result = calculatePrice(100, 100);
expect(result).toBe(0);
});
it('should throw error for negative discount', () => {
expect(() => calculatePrice(100, -10)).toThrow('Invalid discount');
});
});
Mi stack para unit testing
// package.json
{
"devDependencies": {
"@testing-library/react": "^14.0.0",
"@testing-library/jest-dom": "^6.0.0",
"jest": "^29.0.0",
"jest-environment-jsdom": "^29.0.0"
}
}
Configuración Jest + React Testing Library
// jest.config.js
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/src/test/setup.ts'],
moduleNameMapping: {
'^@/(.*)$': '<rootDir>/src/$1',
},
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/test/**',
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
};
2. Testing de componentes: UI que funciona
Testing componentes React con Testing Library
// components/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';
describe('Button', () => {
it('renders with correct text', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument();
});
it('calls onClick when clicked', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);
fireEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('is disabled when loading', () => {
render(<Button loading>Click me</Button>);
expect(screen.getByRole('button')).toBeDisabled();
});
it('shows loading spinner when loading', () => {
render(<Button loading>Click me</Button>);
expect(screen.getByTestId('loading-spinner')).toBeInTheDocument();
});
});
Testing hooks personalizados
// hooks/useCounter.test.ts
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
describe('useCounter', () => {
it('should initialize with 0', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
});
it('should increment count', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
it('should decrement count', () => {
const { result } = renderHook(() => useCounter(5));
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(4);
});
});
3. Testing de integración: APIs y bases de datos
Testing APIs con MSW (Mock Service Worker)
// test/mocks/handlers.ts
import { rest } from 'msw';
export const handlers = [
rest.get('/api/users', (req, res, ctx) => {
return res(
ctx.json([
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Doe', email: 'jane@example.com' },
])
);
}),
rest.post('/api/users', async (req, res, ctx) => {
const { name, email } = await req.json();
return res(
ctx.json({
id: 3,
name,
email,
createdAt: new Date().toISOString(),
})
);
}),
];
Testing base de datos con Supertest
// test/integration/auth.test.ts
import request from 'supertest';
import { app } from '../../src/app';
import { prisma } from '../../src/lib/prisma';
describe('Authentication API', () => {
beforeEach(async () => {
await prisma.user.deleteMany();
});
it('should register a new user', async () => {
const response = await request(app)
.post('/api/auth/register')
.send({
email: 'test@example.com',
password: 'password123',
name: 'Test User',
});
expect(response.status).toBe(201);
expect(response.body.user).toHaveProperty('id');
expect(response.body.user.email).toBe('test@example.com');
});
it('should login existing user', async () => {
// Create user first
await prisma.user.create({
data: {
email: 'test@example.com',
password: await hash('password123', 10),
name: 'Test User',
},
});
const response = await request(app)
.post('/api/auth/login')
.send({
email: 'test@example.com',
password: 'password123',
});
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('token');
});
});
4. Testing E2E: La experiencia del usuario completa
Playwright: Mi herramienta favorita para E2E
// e2e/auth.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Authentication Flow', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/auth/login');
});
test('should login successfully', async ({ page }) => {
// Fill login form
await page.fill('[data-testid="email-input"]', 'user@example.com');
await page.fill('[data-testid="password-input"]', 'password123');
// Click login button
await page.click('[data-testid="login-button"]');
// Wait for navigation and verify
await page.waitForURL('/dashboard');
await expect(page.locator('[data-testid="welcome-message"]')).toContainText('Welcome');
});
test('should show error for invalid credentials', async ({ page }) => {
await page.fill('[data-testid="email-input"]', 'invalid@example.com');
await page.fill('[data-testid="password-input"]', 'wrongpassword');
await page.click('[data-testid="login-button"]');
await expect(page.locator('[data-testid="error-message"]')).toBeVisible();
await expect(page.locator('[data-testid="error-message"]')).toContainText('Invalid credentials');
});
test('should navigate to register page', async ({ page }) => {
await page.click('[data-testid="register-link"]');
await page.waitForURL('/auth/register');
});
});
Configuración Playwright
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
webServer: {
command: 'npm run dev',
port: 3000,
reuseExistingServer: !process.env.CI,
},
});
5. Testing visual: UI consistente
Chromatic + Storybook
// .storybook/main.ts
import type { StorybookConfig } from '@storybook/nextjs';
const config: StorybookConfig = {
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
addons: [
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'@storybook/addon-a11y',
],
framework: {
name: '@storybook/nextjs',
options: {},
},
};
export default config;
Story con testing visual
// components/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta = {
title: 'UI/Button',
component: Button,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
children: 'Click me',
variant: 'primary',
},
};
export const Loading: Story = {
args: {
children: 'Loading...',
loading: true,
},
};
export const Disabled: Story = {
args: {
children: 'Disabled',
disabled: true,
},
};
6. Testing de performance: Rendimiento garantizado
Lighthouse CI
# .github/workflows/lighthouse.yml
name: Lighthouse CI
on: [push, pull_request]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '18'
- name: Build
run: npm run build
- name: Serve and test
run: |
npm install -g lighthouse
npx serve dist -l 3000 &
sleep 5
lighthouse http://localhost:3000 --output=json --output-path=./lighthouse-results.json
- name: Upload results
uses: actions/upload-artifact@v3
with:
name: lighthouse-results
path: ./lighthouse-results.json
Testing de performance con Playwright
// e2e/performance.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Performance Tests', () => {
test('should load homepage quickly', async ({ page }) => {
const startTime = Date.now();
await page.goto('/');
await page.waitForLoadState('networkidle');
const loadTime = Date.now() - startTime;
expect(loadTime).toBeLessThan(3000); // Less than 3 seconds
});
test('should have good Core Web Vitals', async ({ page }) => {
await page.goto('/');
// Measure LCP
const lcp = await page.evaluate(() => {
return new Promise((resolve) => {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
resolve(lastEntry.startTime);
});
observer.observe({ entryTypes: ['largest-contentful-paint'] });
});
});
expect(lcp).toBeLessThan(2500); // Less than 2.5 seconds
});
});
Estrategia de testing: Mi workflow diario
Desarrollo con TDD (Test Driven Development)
// 1. Escribir test primero
test('should format currency correctly', () => {
expect(formatCurrency(1234.56)).toBe('€1,234.56');
});
// 2. Ver test fallar
// 3. Implementar función mínima
function formatCurrency(amount: number): string {
return `€${amount}`;
}
// 4. Ver test pasar
// 5. Refactorizar
function formatCurrency(amount: number): string {
return new Intl.NumberFormat('es-ES', {
style: 'currency',
currency: 'EUR',
}).format(amount);
}
CI/CD con testing automático
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Run unit tests
run: npm run test:unit
- name: Run integration tests
run: npm run test:integration
- name: Run E2E tests
run: npm run test:e2e
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
Coverage y calidad de código
// jest.config.js
module.exports = {
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/index.tsx',
'!src/test/**',
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
coverageReporters: ['text', 'lcov', 'html'],
};
Herramientas y stack recomendado
| Tipo | Herramienta | Por qué | Alternativas |
|---|---|---|---|
| Unit | Jest + RTL | Rápido, confiable | Vitest, Mocha |
| Component | Testing Library | User-centric | Enzyme |
| Integration | Supertest + MSW | Realista | TestCafe |
| E2E | Playwright | Completo, rápido | Cypress |
| Visual | Chromatic | Automatizado | Percy, Applitools |
| Performance | Lighthouse CI | Estándar | WebPageTest |
Casos reales: Errores que evité con testing
Caso 1: Bug de precios en e-commerce
Sin testing: Un cálculo de descuento fallaba en casos edge, causando pérdidas de €2,000+.
Con testing:
it('should handle complex discount scenarios', () => {
// Test que descubrió el bug antes de producción
expect(calculateDiscount(99.99, 50)).toBe(49.995); // Not 50.00
});
Caso 2: Formulario que perdía datos
Problema: Formulario de contacto perdía datos al cambiar de página.
Testing E2E que lo detectó:
test('should persist form data on navigation', async ({ page }) => {
await page.fill('[name="email"]', 'test@example.com');
await page.click('[href="/other-page"]');
await page.goBack();
await expect(page.locator('[name="email"]')).toHaveValue('test@example.com');
});
Caso 3: API que fallaba en producción
Testing de integración que salvó el día:
it('should handle API timeouts gracefully', async () => {
// Mock API timeout
mockServer.use(
rest.get('/api/data', async (req, res, ctx) => {
await new Promise(resolve => setTimeout(resolve, 31000)); // 31s
return res(ctx.json({ data: [] }));
})
);
// Test que la app maneja el timeout
await expect(fetchData()).rejects.toThrow('Request timeout');
});
Mejores prácticas y consejos
1. Testing Pyramid
E2E Tests (pocos, lentos)
↕️
Integration Tests (medianos)
↕️
Unit Tests (muchos, rápidos)
2. Nombres descriptivos
// ❌ Mal
test('should work', () => { ... });
// ✅ Bien
test('should calculate total price with tax and discount', () => { ... });
3. Arrange, Act, Assert
test('should update user profile', () => {
// Arrange
const user = { id: 1, name: 'John' };
const updates = { name: 'Jane' };
// Act
const result = updateUser(user, updates);
// Assert
expect(result.name).toBe('Jane');
});
4. Test data builders
// test/utils/builders.ts
export function buildUser(overrides = {}) {
return {
id: 1,
name: 'John Doe',
email: 'john@example.com',
role: 'user',
...overrides,
};
}
// Uso en tests
const adminUser = buildUser({ role: 'admin' });
5. Page Objects para E2E
// e2e/pages/LoginPage.ts
export class LoginPage {
constructor(private page: Page) {}
async goto() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.page.fill('[data-testid="email"]', email);
await this.page.fill('[data-testid="password"]', password);
await this.page.click('[data-testid="login-button"]');
}
async getErrorMessage() {
return this.page.textContent('[data-testid="error-message"]');
}
}
Conclusión: Testing como inversión
El testing no es un costo, es una inversión.
He visto desarrolladores que pierden horas debuggeando en producción. Yo invierto tiempo en tests y mi código funciona.
¿Quieres implementar testing en tu proyecto? Contáctame y te ayudo a configurar el stack completo.
Preguntas frecuentes (FAQ)
¿Qué herramientas recomiendas para E2E en 2025?
Playwright para velocidad, trazas y paralelización; Cypress sigue siendo buena opción si ya está en el stack.
¿Cuál es la cobertura mínima recomendable?
Objetivo general: 80% global. Prioriza lógica crítica y flujos de negocio (checkout, auth, pagos).
¿Cómo integro testing en CI/CD?
Pipeline: lint + unit + integración + E2E en pull requests; usa GitHub Actions con matrices y caché para velocidad.
¿Sirve para proyectos pequeños o MVP?
Sí: tests unitarios básicos + 2-3 E2E críticos reducen regresiones y tiempo de soporte.
Recursos relacionados
- Herramientas de IA para desarrollo web: Stack completo 2025
- Astro vs Next.js: ¿Cuál elegir para tu proyecto web en 2025?
- Claude vs ChatGPT para programadores: ¿Cuál es mejor en 2025?
Publicado el 16 de diciembre de 2025 por Adrián Pozo Esteban - Desarrollador Full Stack Freelance especializado en testing automatizado, QA y desarrollo web con calidad garantizada en Valencia, España

