Destino Dev

Blog

Testando Guards e Interceptors no NestJS com Jest e SuperTest — do jeito errado ao certo

Por John Pires · 08 de março de 2026

Introducao ao problema

O que são Guards e Interceptors

Guards e Interceptors são peças centrais do pipeline de requisições do NestJS. Guards decidem se a requisição pode prosseguir (autorização, verificação de headers, flags) e são executados antes dos handlers. Interceptors permitem transformar dados de entrada/saída, medir tempo de execução, ou manipular o fluxo reativo usando RxJS.

Para iniciantes, pense no Guard como um porteiro de prédio: ele verifica documentos e decide se você entra. O Interceptor é como o porteiro que, além de deixar entrar, embala sua encomenda antes de entregar — modifica o que vai para o cliente. Testar esses componentes garante que o porteiro e o empacotador não vão estragar sua API em produção.

Por que testar e o que frequentemente é feito errado

Muitos começam testando apenas controllers e serviços, ignorando Guards/Interceptors. Erro comum: testar apenas isoladamente sem integração e assumir que comportamento observável está correto. Outro erro: escrever testes que dependem de implementação interna do Nest (ExecutionContext real), ou executar testes e2e sem simular headers corretos, o que gera falsos negativos/positivos.

Tip: Testes unitários validam lógica isolada do Guard/Interceptor; testes e2e validam integração entre controller, guard e interceptor com SuperTest.

Pre-requisitos e setup

Dependências necessárias

Para seguir os exemplos você precisa de um projeto NestJS básico com Jest e SuperTest. As dependências essenciais (além do @nestjs/core/cli) são:

  • @nestjs/common, @nestjs/core
  • jest, @types/jest, ts-jest
  • supertest
  • rxjs

Abaixo um package.json mínimo usado neste tutorial. Antes do código: este arquivo configura scripts para testes unitários e e2e; em projetos reais você terá divisão de configs de jest (unit vs e2e).

{
  "name": "nest-guard-interceptor-test",
  "version": "1.0.0",
  "scripts": {
    "start": "nest start",
    "test": "jest",
    "test:e2e": "jest --config ./test/jest-e2e.json"
  },
  "dependencies": {
    "@nestjs/common": "^9.0.0",
    "@nestjs/core": "^9.0.0",
    "reflect-metadata": "^0.1.13",
    "rxjs": "^7.0.0"
  },
  "devDependencies": {
    "jest": "^29.0.0",
    "ts-jest": "^29.0.0",
    "supertest": "^6.0.0",
    "@types/jest": "^29.0.0",
    "@types/supertest": "^2.0.12",
    "typescript": "^4.0.0"
  }
}

Após instalar dependências, crie a estrutura mínima de módulos e um controller para testar: AppModule, HelloController.

Estrutura do projeto

Mantenha uma estrutura simples para seguir os exemplos:

  1. src/app.module.ts
  2. src/hello/hello.controller.ts
  3. src/guards/api-key.guard.ts
  4. src/interceptors/response.interceptor.ts
  5. test/app.e2e-spec.ts

Essa estrutura facilita tanto testes unitários quanto e2e com SuperTest.

// Exemplo de controller: src/hello/hello.controller.ts
import { Controller, Get, UseGuards, UseInterceptors } from '@nestjs/common';
import { APIKeyGuard } from '../guards/api-key.guard';
import { ResponseTransformInterceptor } from '../interceptors/response.interceptor';

@Controller('hello')
@UseGuards(APIKeyGuard)
@UseInterceptors(ResponseTransformInterceptor)
export class HelloController {
  @Get()
  getHello() {
    return { message: 'hello world' };
  }
}

Este controller será usado nos exemplos de testes e2e: protegido por um guard que exige um header e transformado por um interceptor.

Tutorial guiado passo a passo

Criando um Guard simples — modo errado

Primeiro o jeito errado: implementar um guard que lança exceções de forma inconsistente e usa objetos do runtime sem abstração. Isso cria dificuldades em testes e perda de controle do retorno booleano esperado pelo Nest.

// src/guards/api-key.guard.ts  (jeito errado)
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';

@Injectable()
export class ApiKeyGuardWrong implements CanActivate {
  canActivate(context: ExecutionContext) {
    const req = context.switchToHttp().getRequest();
    // Erro: lança exceção sem garantir formato e não retorna booleano
    if (!req.headers['x-api-key']) {
      throw new UnauthorizedException('no key');
    }
    if (req.headers['x-api-key'] !== 'secret') {
      // Erro: retorna a exceção em vez de false — confunde quem testa
      return new UnauthorizedException('invalid');
    }
    // Esqueceu de retornar true em paths corretos
  }
}

O problema aqui: canActivate deve retornar boolean | Promise | Observable. Retornar uma exceção ou não retornar nada causa comportamento indefinido e torna testes frágteis.

Criando um Guard simples — modo certo

Agora a versão correta. Observações: sempre retorne boolean, use exceptions quando preciso, e mantenha a lógica simples para facilitar teste unitário.

// src/guards/api-key.guard.ts  (correto)
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';

@Injectable()
export class APIKeyGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const req = context.switchToHttp().getRequest();
    const key = req.headers['x-api-key'];

    if (!key) {
      throw new UnauthorizedException('x-api-key header missing');
    }

    const allowed = key === 'secret';
    if (!allowed) {
      throw new UnauthorizedException('invalid x-api-key');
    }

    return true;
  }
}

O guard agora é previsível: lança UnauthorizedException quando faltar ou estiver errado, e retorna true quando a chave estiver correta. Isso facilita testes unitários (asserts claros) e e2e (esperar 401 quando faltar key).

Criando um Interceptor — modo errado

Interceptors costumam trabalhar com Observables. Um erro comum é transformar o retorno sem usar o fluxo reativo, quebrando compatibilidade com handlers assíncronos.

// src/interceptors/response.interceptor.ts  (jeito errado)
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';

@Injectable()
export class ResponseInterceptorWrong implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler) {
    // Erro: tenta acessar diretamente o valor retornado (não é síncrono)
    const result = next.handle();
    // Isso não funciona: result é um Observable
    return { data: result, executedAt: new Date().toISOString() };
  }
}

Isso falha porque next.handle() retorna um Observable. O interceptor deve retornar um Observable modificado.

Criando um Interceptor — modo certo

Use RxJS map para transformar o fluxo corretamente. Assim o interceptor funciona com Promises, Observables e handlers síncronos.

// src/interceptors/response.interceptor.ts  (correto)
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable()
export class ResponseTransformInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const now = Date.now();
    return next.handle().pipe(
      map((data) => {
        return {
          data,
          executedAt: new Date(now).toISOString(),
        };
      }),
    );
  }
}

O interceptor transforma qualquer resposta em {data, executedAt}. Em testes e2e você validará ambos os campos; em unit você mockará next.handle() retornando of(value).

Escrevendo testes com Jest e SuperTest — testes unitários

Testar Guard isolado: crie um mock de ExecutionContext que expõe switchToHttp().getRequest(). Use objetos simples para evitar dependências do Nest.

// test/api-key.guard.spec.ts
import { APIKeyGuard } from '../src/guards/api-key.guard';
import { ExecutionContext } from '@nestjs/common';

function createContext(headers: Record<string, any>): ExecutionContext {
  return ({
    switchToHttp: () => ({
      getRequest: () => ({ headers }),
    }),
  } as unknown) as ExecutionContext;
}

describe('APIKeyGuard', () => {
  const guard = new APIKeyGuard();

  it('deve negar quando faltar header', () => {
    const ctx = createContext({});
    expect(() => guard.canActivate(ctx)).toThrow();
  });

  it('deve negar quando key errada', () => {
    const ctx = createContext({ 'x-api-key': 'wrong' });
    expect(() => guard.canActivate(ctx)).toThrow();
  });

  it('deve permitir quando key correta', () => {
    const ctx = createContext({ 'x-api-key': 'secret' });
    expect(guard.canActivate(ctx)).toBe(true);
  });
});

Esse é um teste unitário padrão: rápido e sem servidor HTTP.

Escrevendo testes com Jest e SuperTest — testes unitários do Interceptor

Mockeie CallHandler retornando of(value) do RxJS. Isso permite testar transformação sem iniciar servidor.

// test/response.interceptor.spec.ts
import { ResponseTransformInterceptor } from '../src/interceptors/response.interceptor';
import { of } from 'rxjs';

describe('ResponseTransformInterceptor', () => {
  it('deve encapsular a resposta', (done) => {
    const interceptor = new ResponseTransformInterceptor();

    const mockCallHandler = { handle: () => of({ message: 'ok' }) };
    const mockContext: any = {};

    interceptor.intercept(mockContext, mockCallHandler as any).subscribe((result) => {
      expect(result).toHaveProperty('data');
      expect(result.data).toEqual({ message: 'ok' });
      expect(result).toHaveProperty('executedAt');
      done();
    });
  });
});

Teste simples que valida a transformação do interceptor sem dependências externas.

Escrevendo testes com Jest e SuperTest — testes e2e (integração)

Os testes e2e usam SuperTest para fazer requisições reais ao INestApplication. Eles validam a integração: Guard + Interceptor + Controller. Veja exemplo funcional abaixo.

// test/app.e2e-spec.ts
import { Test } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';

describe('App (e2e)', () => {
  let app: INestApplication;

  beforeAll(async () => {
    const moduleRef = await Test.createTestingModule({ imports: [AppModule] }).compile();
    app = moduleRef.createNestApplication();
    await app.init();
  });

  afterAll(async () => {
    await app.close();
  });

  it('/hello 401 sem header', () => {
    return request(app.getHttpServer()).get('/hello').expect(401);
  });

  it('/hello 200 com header e corpo transformado', () => {
    return request(app.getHttpServer())
      .get('/hello')
      .set('x-api-key', 'secret')
      .expect(200)
      .expect((res) => {
        if (!res.body.data) throw new Error('data missing');
        if (!res.body.executedAt) throw new Error('executedAt missing');
        if (res.body.data.message !== 'hello world') throw new Error('unexpected payload');
      });
  });
});

Esse teste confirma que quando faltar o header a API responde 401, e que com header correto a resposta é transformada pelo interceptor.

Resultado final funcionando

Executando os testes

Para executar testes unitários:

  • npm run test

E para e2e (se configurado):

  • npm run test:e2e

Saída esperada (resumida):

PASS  test/api-key.guard.spec.ts
PASS  test/response.interceptor.spec.ts
PASS  test/app.e2e-spec.ts

Se um teste e2e falhar com 500, inspecione logs do Nest; erros comuns incluem Application não inicializado ou rotas não registradas.

O que acontece quando tudo está certo

Com o Guard correto, requests sem header recebem 401 e não chegam ao controller. Com o Interceptor correto, respostas do controller são encapsuladas em um objeto previsível. Testes cobrem ambos os lados: unidade e integração. Isso reduz regressões e facilita mudanças no pipeline.

Erros comuns, armadilhas e dicas práticas

Erros comuns ao testar Guards

Listagem dos erros recorrentes:

  • Tentar usar um ExecutionContext real em unit tests (complexo de mockar) — prefira criar um objeto mínimo que exponha switchToHttp().getRequest().
  • Retornar objetos/Exceptions em vez de boolean no canActivate — cria testes confusos.
  • Ignorar casos assíncronos (quando canActivate retorna Promise) — trate com async/await nos testes.

Erros comuns ao testar Interceptors

Erros frequentes:

  • Tentar acessar next.handle() como valor; lembrar que é um Observable.
  • Não tratar unsubscribe em testes que usam Observables — prefira usar done() em Jest ou toPromise()/firstValueFrom quando necessário.
  • Testar apenas o tipo em vez de validar conteúdo transformado.
Dica prática: mantenha os guards e interceptors pequenos e com uma única responsabilidade. Components fáceis de testar tendem a ter menos bugs.

Desafio para o leitor praticar

Tarefas propostas

Implemente as seguintes melhorias no projeto:

  1. Adicione um guard que aceite múltiplas chaves válidas vindo de config (e escreva testes unitários cobrindo ambas chaves).
  2. Modifique o interceptor para adicionar um campo de requestId único (uuid) e escreva testes que garantam que requestId é consistente entre logs e resposta.
  3. Crie testes que simulem tempo de execução alto no handler e valide que o interceptor registra (ou retorna) o tempo de execução corretamente.

Critérios de aceitação

Para considerar os desafios completos:

  • Todos os testes unitários devem passar sem acessar o servidor.
  • Os testes e2e devem validar fluxo autenticado e não autenticado.
  • As mudanças devem manter a API externa (rota /hello) com o mesmo formato de resposta.

Conclusao pratica

Resumo dos pontos principais

Testar Guards e Interceptors é simples quando você separa responsabilidades: escreva guards que retornam boolean/lançam exceptions previsíveis; escreva interceptors que operam no fluxo reativo (Observable). Combine testes unitários (mock ExecutionContext/CallHandler) com testes e2e (SuperTest) para garantir comportamento observado.

Checklist rápido para revisão de PR

Antes de aprovar um pull request que envolve guards/interceptors, verifique:

  • Guards retornam boolean | Promise e lançam exceptions nos casos esperados.
  • Interceptors usam pipe(map()) ou operadores RxJS apropriados e não manipulam valores síncronos diretamente.
  • Existem testes unitários cobrindo caminhos positivos e negativos.
  • Se alterou comportamento observável, os testes e2e foram atualizados.

Aplicando esses princípios você reduz bugs em produção e torna o código mais previsível para novos desenvolvedores. Testes bem escritos funcionam como documentação executável do comportamento do pipeline do NestJS.

Este artigo mostrou o jeito errado e o jeito certo de implementar e testar Guards e Interceptors no NestJS, com exemplos práticos de unit e e2e usando Jest e SuperTest. Siga os exemplos, implemente os desafios e revise PRs com o checklist para consolidar o aprendizado.

Assuntos: NestJS, guards, interceptors, Testes, supertest

Leia também