Testing Guards and Controllers

A Guard Unit Test

Guards take an ExecutionContext; build a small fake one.

src/auth/roles.guard.spec.ts

import { ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { RolesGuard } from './roles.guard';

const contextFor = (user: unknown): ExecutionContext =>
  ({
    getHandler: () => () => undefined,
    getClass: () => class {},
    switchToHttp: () => ({ getRequest: () => ({ user }) }),
  }) as unknown as ExecutionContext;

describe('RolesGuard', () => {
  const reflector = { getAllAndOverride: jest.fn() } as unknown as Reflector;
  const guard = new RolesGuard(reflector);

  it('allows when no roles are required', () => {
    (reflector.getAllAndOverride as jest.Mock).mockReturnValue(undefined);
    expect(guard.canActivate(contextFor({ roles: ['customer'] }))).toBe(true);
  });

  it('blocks a customer on an admin route', () => {
    (reflector.getAllAndOverride as jest.Mock).mockReturnValue(['admin']);
    expect(guard.canActivate(contextFor({ roles: ['customer'] }))).toBe(false);
  });
});

A Controller Unit Test

describe('ProductsController', () => {
  let controller: ProductsController;
  const service = { findAll: jest.fn().mockResolvedValue([{ id: 7 }]) };

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      controllers: [ProductsController],
      providers: [{ provide: ProductsService, useValue: service }],
    }).compile();
    controller = module.get(ProductsController);
  });

  it('delegates to the service', async () => {
    await expect(controller.findAll('audio')).resolves.toEqual([{ id: 7 }]);
    expect(service.findAll).toHaveBeenCalledWith('audio');
  });
});

Tip: Controller unit tests do not run pipes, guards or interceptors. Test those behaviours (400 on bad body, 401 without a token) in e2e tests.