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 2026

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

Actualizado: 29 de marzo de 2026
20 min de lectura
Testing y QA automatizado desarrollo web freelance Valencia: Guía completa Jest, Playwright, Cypress 2026

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

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 2026?
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 2026 por Adrián Pozo Esteban - Desarrollador Full Stack Freelance especializado en testing automatizado, QA y desarrollo web con calidad garantizada en Valencia, España

📍
📍

¿Proyecto en Valencia o alrededores?

Desarrollo web, apps móviles y tiendas online en Valencia. Conocimiento del ecosistema local (Lanzadera, CEEI). Presupuesto sin compromiso.

Ver servicios Valencia

Compartir este artículo

¿Te ha sido útil este artículo?

Publico contenido sobre desarrollo web, apps y freelancing en LinkedIn. Sígueme para no perderte los próximos.

Seguir en LinkedIn

Artículos Relacionados