README · by ansango
← Volver al libro

Testing del backend

Por qué testear el backend, trade-offs de cobertura, tests unit con Jest, mock data, e2e tests con Postman/Cypress y colaboración con QA

~5 min de lectura
Resumen

Esta nota cubre el lado práctico de los tests en el backend: por qué vale la pena invertir tiempo en ellos, qué trade-offs aceptar, cómo escribir unit tests con Jest, qué son los e2e tests, cómo manejar mock data y cómo encaja QA en el flujo. El objetivo es tener un mindset de testing que sobreviva al crecimiento del proyecto, no a una cobertura del 100% que no aporta valor real.

Por qué dedicar tiempo a los tests

A medida que un proyecto crece, las regresiones (cambios que rompen funcionalidad que ya funcionaba) son inevitables. Los tests son la red de seguridad que las caza antes de que lleguen a producción.

En el backend los tests verifican:

Tests como documentación

Un test bien escrito es documentación ejecutable. Muestra cómo se espera que funcione el código, qué inputs son válidos, qué errores se devuelven. Cuando un dev nuevo llega al proyecto, leer los tests es una de las formas más rápidas de entender el sistema.

Trade-offs

Hay developers con opiniones muy fuertes sobre testing. Los puntos de equilibrio importantes:

El espejismo de “lo escribo después”

Decir “lo escribo después para salir del paso” es una trampa. Los tests se van al backlog cuando entran bugs y features, y nunca vuelven. Escríbelos con la implementación.

Vende los tests a Producto

Si tu equipo de Producto no ve el valor de los tests, muéstrales cómo evitan hotfixes. Una vez vean el ROI, van a empezar a preguntar por escenarios adicionales para que escribas tests más completos.

Colaboración con QA

Si tienes un equipo de QA:

Cómo enfocar la escritura de tests

No hace falta ser purista de TDD o BDD. Lo importante es saber qué testear.

Cuando escribes unit tests en el backend, verificas funcionalidad específica:

Recorre el código línea a línea

Tests con el código abierto a la vez

Tener el archivo de test y el archivo de código abiertos en columnas lado a lado ayuda a ver si has cubierto cada rama mientras avanzas por el código.

Cuándo mockear

Qué cubrir con los tests

Tests con Jest

Jest es la elección estándar en proyectos NestJS. Veamos un ejemplo sobre un controller.

El código bajo test

@Post()
public async create(@Body() user: User, order: CreateOrderDto): Promise<Order> {
  if (!user) {
    throw new HttpException('Unauthorized', HttpStatus.UNAUTHORIZED);
  }
  if (!user.permissions.includes('create:orders')) {
    throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
  }
  if (!order) {
    throw new HttpException('No order data', HttpStatus.BAD_REQUEST);
  }
  if (order.products.length === 0) {
    throw new HttpException('No products in order', HttpStatus.CONFLICT);
  }
  if (!order.total) {
    throw new HttpException('No order total', HttpStatus.CONFLICT);
  }
  try {
    const newOrder = await this.ordersService.createOrder(order);
    return newOrder;
  } catch (err) {
    throw new HttpException('Something happened', HttpStatus.NOT_FOUND);
  }
}

Test suite

import { Test, TestingModule } from '@nestjs/testing';
import { OrdersV1Controller } from './orders.controller';
import { OrdersService } from './orders.service';

const user = {
  id: 1001,
  email: '[email protected]',
  name: 'Tester Rest',
  permissions: ['get:orders', 'create:orders'],
};

const order = {
  name: 'Biggest order',
  total: 125.99,
  stripeInvoiceId: 'stripeInvoiceId',
};

describe('OrdersController', () => {
  let controller: OrdersV1Controller;
  let ordersService: OrdersService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      controllers: [OrdersV1Controller],
      providers: [OrdersService],
    }).compile();
    controller = module.get<OrdersV1Controller>(OrdersV1Controller);
    ordersService = module.get<OrdersService>(OrdersService);
  });

  it('throws unauthorized error if the user is undefined', async () => {
    await controller.create(undefined, order);
    expect(controller.create).toThrowError('Unauthorized');
  });

  it('throws forbidden error if the user does not have correct permissions', async () => {
    const badPermissionsUser = {
      id: 1001,
      email: '[email protected]',
      name: 'Tester Rest',
      permissions: ['get:products'],
    };
    await controller.create(badPermissionsUser, order);
    expect(controller.create).toThrowError('Forbidden');
  });

  it('throws bad request error if the order is undefined', async () => {
    await controller.create(user, undefined);
    expect(controller.create).toThrowError('No order data');
  });

  it('throws conflict error if products are missing from the order', async () => {
    const orderWithoutName = {
      total: 125.99,
      stripeInvoiceId: 'stripeInvoiceId',
    };
    await controller.create(user, orderWithoutName);
    expect(controller.create).toThrowError('No order name');
  });

  it('throws conflict error if the order total is missing', async () => {
    const orderWithoutTotal = {
      name: 'Biggest order',
      stripeInvoiceId: 'stripeInvoiceId',
    };
    await controller.create(user, orderWithoutTotal);
    expect(controller.create).toThrowError('No order total');
  });

  it('successfully creates a new order', async () => {
    controller.create(user, order);
    const newOrder = await ordersService.createOrder(undefined);
    expect(ordersService.createOrder).toBeCalledWith(order);
    expect(ordersService.createOrder).toReturnWith(newOrder);
  });

  it('throws not found error if something happens in the service', async () => {
    try {
      controller.create(user, order);
      await ordersService.createOrder(undefined);
      expect(ordersService.createOrder).toThrowError();
    } catch (e) {
      expect(e.message).toBe('Something happened');
    }
  });
});

De una función con cinco errores posibles y un happy path salen siete tests. Esa es la mecánica: cada rama de código se traduce en al menos un test.

AI tools para tests

Herramientas como Copilot o ChatGPT son útiles para acelerar la escritura de tests o generar escenarios que no habías pensado. No las uses como sustituto de entender qué estás testeando, pero sí como acelerador.

Mensajes de error

Mensaje corto y útil

El mensaje de error que devuelve tu API al frontend debe ser breve y específico, sin filtrar PII ni info que un atacante pueda aprovechar. El status code asociado va aparte.

e2e tests

Los e2e tests verifican el flujo completo de un usuario, no la implementación. Hay varias formas de hacerlos:

it('/v1/orders (POST)', () => {
  return request(app.getHttpServer())
    .post('/v1/orders')
    .expect(200)
    .expect(order);
});
“e2e” significa cosas distintas para cada uno

Para algunos, e2e implica frontend + backend. Para otros, e2e puede ser solo backend si verifica funcionalidad de alto nivel en lugar de implementación. No hay una definición canónica. Acuerda qué significa en tu equipo.

Mock data

El mock data cubre los escenarios que quieres testear, no solo el caso feliz.

Tipos de mocks

Test data factories

Ethan Brown lo explica bien: en lugar de que cada dev reinvente el objeto user cada vez, define factories que devuelven datos realistas. Algo como FakeUserFactory.create(10) te da 10 usuarios con datos que podrían existir en producción.

Test data que cubra casos reales

Si tienes un archivo con 15 usuarios ficticios para elegir, asegúrate de incluir casos raros: nombres ridículamente largos, nombres de una palabra, caracteres no latinos. Cuando un junior dev use uno de esos, recordará que tiene que manejar nombres extremos.

Buena cobertura con poco código

Por cada test quieres:

Con eso cubres el caso principal. Los edge cases se van añadiendo según los encuentras (y según hablas con Producto y QA sobre qué se considera “caso raro”).

Próximos pasos