La mayoría de los equipos tiene una versión de esta conversa: "Nuestra cobertura de tests está en 87%, pero cada deploy es estresante." El número es alto, pero la confianza es baja. ¿Qué está mal?

La cobertura de código mide cuántas líneas fueron ejecutadas durante los tests. No mide si los tests verifican comportamiento correcto, si cubren los casos que importan, o si un refactor que rompe la aplicación va a ser detectado. Puedes tener 100% de cobertura y una suite de tests que no testea nada útil.

La pirámide de tests revisitada

El modelo clásico de pirámide, muchos unit tests, pocos integration tests, menos E2E, todavía es válido como principio, pero se aplica mal en la práctica.

El error más común: escribir cientos de unit tests que testean implementación, no comportamiento. Cuando el código es refactorizado, todos los tests se rompen, aunque el comportamiento externo no haya cambiado. Esto genera un feedback negativo fuerte: refactorización = dolor. Los equipos que pasan por esto dejan de refactorizar.

El principio correcto, popularizado por Kent C. Dodds: mientras más tus tests se parecen a la forma en que el software es usado, más confianza dan.

Testear comportamiento, no implementación

La distinción práctica:

// ❌ Testea implementación: frágil
it('should call setLoading(true) then fetch then setLoading(false)', () => {
  const setLoading = jest.fn()
  // ... testea detalles internos
})

// ✅ Testea comportamiento: confiable
it('shows spinner while loading, then displays results', async () => {
  render(<ProductList />)

  expect(screen.getByRole('progressbar')).toBeInTheDocument()

  await screen.findByText('Product A')

  expect(screen.queryByRole('progressbar')).not.toBeInTheDocument()
  expect(screen.getByText('Product A')).toBeInTheDocument()
})

El primer test se rompe en cualquier refactor de la implementación. El segundo sobrevive a cualquier refactor que preserva el comportamiento visible.

Los casos que importan cubrir

En lugar de perseguir porcentaje, piensa en categorías de cobertura que importan:

Camino feliz

¿El flujo principal funciona? ¿Input válido produce output esperado? Es el mínimo, necesario pero no suficiente.

Casos de borde

¿Qué pasa con input vacío? ¿Con valores extremos? ¿Con null donde no se espera? ¿Con strings muy largas? La mayoría de los bugs reales vive aquí.

Casos de error

¿Qué pasa cuando la API externa falla? ¿Cuando la base está indisponible? ¿Cuando el usuario no tiene permiso? Si no testeas los caminos de error, no sabes qué van a ver tus usuarios cuando algo salga mal.

Invariantes de negocio

Las reglas que nunca pueden ser violadas: el saldo no puede quedar negativo, un usuario no puede acceder a datos de otro usuario, un pedido no puede aprobarse sin item. Estos tests son la documentación ejecutable de las reglas de negocio.

Integration tests: el sweet spot ignorado

Para la mayoría de las aplicaciones, los integration tests son donde el ROI de los tests es mayor. Un integration test que ejercita el endpoint real, con base de datos en memoria o de test, cubre más comportamiento con menos código que docenas de unit tests aislados.

// Integration test con base de test: alta confianza
describe('POST /orders', () => {
  it('creates order and deducts inventory', async () => {
    await db.product.create({ id: 'p1', stock: 5 })

    const res = await request(app)
      .post('/orders')
      .set('Authorization', `Bearer ${userToken}`)
      .send({ productId: 'p1', quantity: 2 })

    expect(res.status).toBe(201)
    expect(res.body.order.status).toBe('confirmed')

    const product = await db.product.findUnique({ where: { id: 'p1' } })
    expect(product.stock).toBe(3)
  })
})

Ese test único valida: la autenticación funciona, la validación de input funciona, la lógica de pedido funciona, la actualización de stock funciona, la respuesta es correcta. Cinco comportamientos en un test.

La métrica correcta para medir confianza

En lugar de cobertura de líneas, mide:

  • ¿Cuántos bugs llegaron a producción que la suite no detectó en los últimos 3 meses? Si la respuesta es alta, tienes gap de cobertura en comportamientos importantes.
  • ¿Cuánto tiempo gastas "arreglando" tests que se rompieron sin que el comportamiento cambiara? Si es alto, tienes tests frágiles que cuestan más de lo que entregan.
  • ¿Haces deploy confiado o rezando? La respuesta dice más sobre la calidad de los tests que cualquier número de cobertura.

Cobertura de 60% con tests bien escritos da más confianza que cobertura de 90% con tests que testean implementación. Optimiza para confianza, no para métrica.