Desarrollo Web #Testing #QA #Automatización #Jest #Playwright #Cypress #Desarrollo Web #Freelance #Valencia

Testing y QA automatizado desarrollo web freelance Valencia: Guía completa Jest, Playwright, Cypress 2025

Testing automatizado desarrollo web freelance Valencia: unitario, integración, E2E, visual y performance. Jest, Playwright, Cypress para proyectos startups España 2025.

20 min de lectura
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

TipoHerramientaPor quéAlternativas
UnitJest + RTLRápido, confiableVitest, Mocha
ComponentTesting LibraryUser-centricEnzyme
IntegrationSupertest + MSWRealistaTestCafe
E2EPlaywrightCompleto, rápidoCypress
VisualChromaticAutomatizadoPercy, Applitools
PerformanceLighthouse CIEstándarWebPageTest

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


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

Compartir este artículo

Artículos Relacionados