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:
- src/app.module.ts
- src/hello/hello.controller.ts
- src/guards/api-key.guard.ts
- src/interceptors/response.interceptor.ts
- 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:
- Adicione um guard que aceite múltiplas chaves válidas vindo de config (e escreva testes unitários cobrindo ambas chaves).
- Modifique o interceptor para adicionar um campo de requestId único (uuid) e escreva testes que garantam que requestId é consistente entre logs e resposta.
- 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
API REST com NestJS, Prisma e PostgreSQL: JWT, testes e otimização de performance
Guia avançado para APIs com NestJS, Prisma e PostgreSQL. Cobre autenticação JWT, testes automáticos (unit/e2e) e otimizações de performance para produção…
Autenticação JWT segura no NestJS com refresh tokens rotativos e Redis
Implementação prática de autenticação JWT no NestJS com refresh tokens rotativos, armazenamento de jti em Redis e detecção de replay. Guia com exemplos de…
Autorização por permissões no NestJS usando decorators e guards
Aprenda a controlar autorização por permissões no NestJS usando decorators e guards. Tutorial prático, passo a passo, com exemplos funcionais, integração JWT…
Interceptor global de logs e tempo no NestJS: implementar e testar
Implemente um interceptor global no NestJS para logs e medição de tempo, registre-o no bootstrap e teste com Jest e supertest. Artigo prático passo a passo…
NestJS: criar e testar microservice TCP e gateway HTTP passo a passo
Passo a passo prático para criar e testar um microservice TCP em NestJS e um gateway HTTP. Mostro o jeito errado, o jeito certo, exemplos funcionais e como…
Rate limiting por usuário no NestJS com Guard + Redis (sliding window)
Aprenda a implementar rate limiting por usuário no NestJS usando Guard + Redis com sliding window. Guia passo a passo, exemplos práticos (Lua + ioredis)…