Como criar um teste unitário para um interceptor - Angular
Explore o passo a passo na criação de testes unitários para interceptors no Angular e descubra a importância desses testes para a qualidade e confiabilidade do seu código.
Interceptors desempenham um papel importante na manipulação de requisições HTTP em aplicações Angular. Por exemplo, é comum encontrarmos interceptors que injetam cabeçalhos de requisição ou que padronizam o tratamento de erros.
Neste artigo, exploraremos como criar testes unitários para interceptors. Os exemplos a seguir utilizam a configuração padrão do Angular, utilizando o Jasmine para a execução dos testes unitários. Se você estiver utilizando o Jest, pode ser necessário fazer pequenos ajustes, mas a lógica empregada nos exemplos será a mesma, independentemente do framework utilizado para os testes unitários.
Teste unitário - Interceptor Authorization
Como primeiro exemplo vamos construir um teste unitário para um interceptor que injeta o cabeçalho Authorization. O código fonte é o seguinte:
import {
HttpHandler,
HttpInterceptor,
HttpRequest,
} from '@angular/common/http';
import { Injectable } from '@angular/core';
import { StorageService } from './storage.service';
@Injectable()
export class AuthorizationInterceptor
implements HttpInterceptor
{
constructor(private storageService: StorageService) {}
intercept(req: HttpRequest<any>, next: HttpHandler) {
const jwt: string | null = this.storageService.getJWT();
if (jwt) {
req = req.clone({
headers: req.headers.set('Authorization', jwt),
});
}
return next.handle(req);
}
}
Analisando o código do interceptor acima, podemos observar dois possíveis caminhos (fluxos):
- O
this.storageService.getJWT()retornará umastring, e assim, o conteúdo dentro do blocoifserá executado para injetar o valor no cabeçalho Authorization. - Se
this.storageService.getJWT()retornarnullou estiver vazio, o conteúdo dentro do blocoifnão será executado, e o cabeçalho de requisição Authorization não será definido.
Observação: Esses dois caminhos são identificados como "branches" durante a análise de cobertura dos testes unitários.
Agora, vamos testar esses dois cenários!
Para iniciar a construção dos testes unitários, é necessário configurar um módulo de testes. Isso pode ser feito utilizando o TestBed fornecido pelo próprio Angular. Através do método configureTestingModule, importaremos as dependências para testar uma unidade de código. Essa configuração será realizada no arquivo chamado authorization.interceptor.spec.ts.
Uma abordagem para testar um interceptor é configurá-lo no módulo de testes para "dizer" ao Angular que deve usar o interceptor em todas as requisições. Em seguida, podemos disparar requisições "mockadas", esperando que o interceptor injete ou não o cabeçalho Authorization. Além disso, será necessário simular o retorno de storageService.getJWT(). Vamos para o código!
A seguir, elaborei a estrutura do teste unitário e adicionei vários comentários para facilitar o entendimento:
import { TestBed } from '@angular/core/testing';
import {
HTTP_INTERCEPTORS,
HttpClient,
HttpStatusCode,
} from '@angular/common/http';
import {
HttpClientTestingModule,
HttpTestingController,
} from '@angular/common/http/testing';
import { AuthorizationInterceptor } from './authorization.interceptor';
import { StorageService } from './storage.service';
describe('AuthorizationInterceptor', () => {
let httpClient: HttpClient;
let httpTestingController: HttpTestingController;
let storageService: StorageService;
// Este trecho é executado antes de cada teste unitário,
// ou seja, antes de cada "it"
beforeEach(() => {
// Configurando o módulo de testes:
TestBed.configureTestingModule({
imports: [
// Como vamos disparar requisições simuladas
// ao longo deste arquivo de testes, precisamos
// importar o módulo abaixo:
HttpClientTestingModule,
],
providers: [
// Configurando o interceptor para
// ser utilizado em todas as requisições:
{
provide: HTTP_INTERCEPTORS,
useClass: AuthorizationInterceptor,
multi: true,
},
],
});
// Pegamos e guardamos alguns objetos para utilizar
// ao longo dos testes:
httpClient = TestBed.inject(HttpClient);
httpTestingController = TestBed.inject(
HttpTestingController
);
storageService = TestBed.inject(StorageService);
});
});
Com a estrutura do teste unitário construída, estamos prontos para criar o primeiro teste. A seguir escrevi um teste em que o AuthorizationInterceptor não injeta o cabeçalho Authorization devido ao retorno null do método getJWT():
import { TestBed } from '@angular/core/testing';
import {
HTTP_INTERCEPTORS,
HttpClient,
HttpStatusCode,
} from '@angular/common/http';
import {
HttpClientTestingModule,
HttpTestingController,
} from '@angular/common/http/testing';
import { AuthorizationInterceptor } from './authorization.interceptor';
import { StorageService } from './storage.service';
describe('AuthorizationInterceptor', () => {
let httpClient: HttpClient;
let httpTestingController: HttpTestingController;
let storageService: StorageService;
// Este trecho é executado antes de cada teste unitário,
// ou seja, antes de cada "it"
beforeEach(() => {
// Configurando o módulo de testes:
TestBed.configureTestingModule({
imports: [
// Como vamos disparar requisições simuladas
// ao longo deste arquivo de testes, precisamos
// importar o módulo abaixo:
HttpClientTestingModule,
],
providers: [
// Configurando o interceptor para
// ser utilizado em todas as requisições:
{
provide: HTTP_INTERCEPTORS,
useClass: AuthorizationInterceptor,
multi: true,
},
],
});
// Pegamos e guardamos alguns objetos para utilizar
// ao longo dos testes:
httpClient = TestBed.inject(HttpClient);
httpTestingController = TestBed.inject(
HttpTestingController
);
storageService = TestBed.inject(StorageService);
});
it(`não deve injetar o header Authorization
quando getJWT() retornar null`, (done) => {
// Simulamos o retorno do método getJWT()
// de modo que o método retorne null.
const storageServiceGetJWTSpy = spyOn(
storageService,
'getJWT'
).and.returnValue(null);
// Dispara uma requisição para a URL abaixo.
// Lembre-se de que estamos utilizando o módulo
// HttpClientTestingModule, então as requisições
// são simuladas ("mockadas")
const url = 'http://localhost';
httpClient.get(url).subscribe({
complete: () => {
// Quando a requisição for respondida, esperamos
// que o storageService.getJWT() seja chamado
// uma única vez:
expect(
storageServiceGetJWTSpy
).toHaveBeenCalledTimes(1);
// Finalizamos o teste chamando o done():
done();
},
});
// Esperamos que tenha sido disparada uma requisição
// para http://localhost e...
const req = httpTestingController.expectOne(url);
// ...também esperamos que o header Authorization não tenha
// sido injetado, já que o retorno do storageService.getJWT()
// é null
expect(
req.request.headers.has('Authorization')
).toBeFalsy();
// Envia a resposta simulada da requisição.
// Assim o método dentro do .subscribe(...método...)
// será executado
req.flush(
{},
{
status: HttpStatusCode.Ok,
statusText: 'Ok',
}
);
});
});
Para executar os testes unitários basta rodar o comando ng test:

O teste unitário foi executado com sucesso. Agora vamos analisar a cobertura de código, ou seja, quais partes do AuthorizationInterceptor foram cobertas pelo nosso teste unitário. Para realizar essa tarefa, basta executar o comando abaixo:
ng test --code-coverage --watch falseApós a execução, será gerada uma pasta chamada /coverage. Abra o arquivo index.html e navegue até o arquivo authorization.interceptor.ts:

Observe na imagem acima que nosso teste unitário não entrou no bloco if, indicando que o código que desenvolvemos para testar o AuthorizationInterceptor não contemplou um cenário em que o conteúdo dentro do bloco if fosse executado. Por essa razão, é exibido um "I" na imagem, localizado à esquerda do bloco if. Esse "I" representa "if path not taken", ou seja, nenhum teste unitário passou pelo bloco if.
Na verdade, fizemos isso de propósito, uma vez que simulamos que o método storageService.getJWT() retorna o valor null. Agora, vamos escrever um segundo teste para contemplar este cenário:
// ...(conteúdo ocultado)...
it('deve injetar o JWT no header Authorization', (done) => {
const jwtValue = '123';
const storageServiceGetJWTSpy = spyOn(
storageService,
'getJWT'
).and.returnValue(jwtValue);
// A URL utilizada no teste pode ser qualquer uma. O
// motivo é que vamos criar uma resposta simulada para
// este endereço utilizando o objeto httpTestingController
const url = 'http://localhost';
httpClient.get(url).subscribe({
next: (response) => {
// Esperamos que a resposta seja exatamente o corpo
// que efetuamos o mock
expect(response).toEqual(responseMock);
},
complete: () => {
// Esperamos que o StorageService.getJWT seja chamado
// uma única vez
expect(
storageServiceGetJWTSpy
).toHaveBeenCalledTimes(1);
done();
},
});
// Aqui criamos uma resposta "mockada" para 'http://localhost'
const req = httpTestingController.expectOne(url);
// Aqui disparamos a resposta "mockada". Essa resposta
// chega no subscribe do httpClient.get
const responseMock = { mensagem: 'ola' };
req.flush(responseMock, {
status: HttpStatusCode.Ok,
statusText: 'Ok',
});
// Esperamos que o cabeçalho "Authorization" esteja presente
// na requisição com o valor mockado que está na variável "jwtValue"
expect(
req.request.headers.has('Authorization')
).toBeTruthy();
expect(req.request.headers.get('Authorization')).toBe(
jwtValue
);
});
// ...(conteúdo ocultado)...Executando novamente o comando abaixo, é possível ver nossos testes unitários cobrem 100% do código do AuthorizationInterceptor:
ng test --code-coverage --watch false
Teste unitário - Interceptor para tratamento de erros
Como segundo caso de estudo, copiei e alterei um trecho de código da documentação oficial para ilustrar a implementação de um interceptor destinado a lidar com erros durante as requisições:
import {
HttpErrorResponse,
HttpEvent,
HttpHandler,
HttpInterceptor,
HttpRequest,
} from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, catchError, throwError } from 'rxjs';
@Injectable()
export class HandleErrorInterceptor
implements HttpInterceptor
{
intercept(
request: HttpRequest<unknown>,
next: HttpHandler
): Observable<HttpEvent<unknown>> {
return next.handle(request).pipe(
catchError((error: HttpErrorResponse) => {
let errorMsg = '';
if (error.status === 0) {
// A client-side or network error
// occurred. Handle it accordingly.
errorMsg = `An error occurred: ${JSON.stringify(
error.error
)}`;
} else {
// The backend returned an unsuccessful response
// code. The response body may contain clues as
// to what went wrong.
errorMsg = `Backend returned code ${error.status}`;
}
return throwError(() => errorMsg);
})
);
}
}
Assim como no interceptor do exemplo anterior, aqui temos duas ramificações (possibilidades) no código:
- Tratamento de erros quando
error.status === 0: Isso ocorre em erros originados no lado do cliente devido à falta de conexão com a internet ou a alguma exceção JavaScript. - Tratamento de erros oriundos de uma resposta do backend.
Vamos abordar o teste unitário para essas situações:
import { TestBed } from '@angular/core/testing';
import {
HTTP_INTERCEPTORS,
HttpClient,
HttpStatusCode,
} from '@angular/common/http';
import {
HttpClientTestingModule,
HttpTestingController,
} from '@angular/common/http/testing';
import { HandleErrorInterceptor } from './handle-error.interceptor';
describe('AuthInterceptor', () => {
let httpClient: HttpClient;
let httpTestingController: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
HandleErrorInterceptor,
{
provide: HTTP_INTERCEPTORS,
useClass: HandleErrorInterceptor,
multi: true,
},
],
});
httpClient = TestBed.inject(HttpClient);
httpTestingController = TestBed.inject(
HttpTestingController
);
});
it('deve tratar erros 4xx', (done) => {
const url = 'http://localhost';
httpClient.get(url).subscribe({
error: (responseError) => {
// Esperamos que o interceptor formate a mensagem
// de erro como abaixo:
const expectedMessage =
`Backend returned code ${HttpStatusCode.BadRequest}`;
expect(responseError).toEqual(expectedMessage);
done();
},
});
const req = httpTestingController.expectOne(url);
// Simulamos uma resposta para a requisição
// ao endereço http://localhost com o status
// 400 (bad request):
req.flush(
{},
{
status: HttpStatusCode.BadRequest,
statusText: 'Erro na validação',
}
);
});
it('deve tratar erros no lado do cliente', (done) => {
const mockError = new ProgressEvent('error');
const url = 'http://localhost';
httpClient.get(url).subscribe({
error: (responseError) => {
// Esperamos que o interceptor formate a
// mensagem de erro e inclua o seguinte
// trecho no retorno:
const expectedMessage = `An error occurred: `;
expect(responseError).toContain(expectedMessage);
done();
},
});
const req = httpTestingController.expectOne(url);
// Simulamos um erro local, ou seja, um erro no lado
// do cliente. Neste caso a requisição nem chega no
// servidor:
req.error(mockError);
});
});
Resultado:

Considerações
A criação de testes unitários para interceptors não é tão complicada quanto parece. Pessoalmente, considero isso uma espécie de "receita de bolo", pois, basicamente, todo teste unitário segue a mesma estrutura. Como sugestão, sempre que possível, desenvolva testes unitários para garantir que o seu código atenda a todos os requisitos. Isso contribuirá para a entrega de uma aplicação mais resiliente e de alta qualidade.
Link do repositório onde vou incluir mais exemplos de testes unitários além dos citados neste post:
Links interessantes: