Como mockar uma service ou dependência de um componente - Angular + JEST
Cenário
Em uma aplicação Angular, efetuar uma consulta HTTP para se obter informações é basicamente uma das tarefas mais comuns no cotidiano do desenvolvedor. Por esta razão em nosso cenário de testes criamos uma service que é injetada no app.component.ts
com o objetivo de mostrar uma lista de estados na tela. Em caso de erro, é exibida uma mensagem (Ocorreu um erro inesperado).
estados.service.ts:
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
const url =
'https://servicodados.ibge.gov.br/api/v1/localidades/estados';
@Injectable({providedIn: 'root'})
export class EstadosService {
constructor(private httpClient: HttpClient) { }
listar(): Observable<any[]> {
return this.httpClient
.get<any[]>(url);
}
}
Apenas para ficar mais claro, ao acessar o link utilizado dentro https://servicodados.ibge.gov.br/api/v1/localidades/estados é retornado um JSON no seguinte formado:
[
{
"id": 11,
"sigla": "RO",
"nome": "Rondônia",
"regiao": {
"id": 1,
"sigla": "N",
"nome": "Norte"
}
},
/* ... Código ocultado ... */
]
app.component.ts
import { Component, OnInit } from '@angular/core';
import { EstadosService } from './services/estados.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
estados: any = [];
ocorreuErro: boolean = false;
constructor(private estadosService: EstadosService) { }
ngOnInit() {
this.estadosService
.listar()
.subscribe(
listaEstados => {
this.ocorreuErro = false;
this.estados = listaEstados;
},
error => {
console.error('Erro ao obter os registros', error);
this.ocorreuErro = true;
}
);
}
}
app.component.html
<html>
<head>
<title>Testes</title>
</head>
<body>
<table *ngIf="!ocorreuErro; else templateErro">
<tr *ngFor="let estado of estados">
<td>{{estado.sigla}}</td>
<td>{{estado.nome}}</td>
</tr>
</table>
<router-outlet></router-outlet>
</body>
</html>
<ng-template #templateErro>
<div class="erro">
Ocorreu um erro inesperado
</div>
</ng-template>
Ao executar a aplicação executando o comando ng serve
e acessar http://localhost:4200
no seu navegador, é possível visualizar o seguinte resultado:
Antes de começar a construção do teste unitário, certifique-se de que no seu arquivo /tsconfig.spec.json
tenha a propriedade "emitDecoratorMetadata": true
:
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"emitDecoratorMetadata": true,
"types": [
"jest",
"node"
]
},
"files": [
"src/polyfills.ts"
],
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}
Construção do teste unitário
Veja que nosso app.component.ts
depende da classe EstadosService
. Como a ideia é testar apenas o comportamento de cada unidade de código do app.component.ts
, temos que mockar de alguma forma a classe EstadosService
para simular os vários comportamentos que queremos testar.
Existe algumas formas de efetuar este trabalho. A primeira delas é criar uma classe de mock com a mesma assinatura da classe EstadosService
e indicar ao Angular que toda vez que EstadosService
for requisitada será entregue uma outra classe.
Definindo um provider
Veja no código abaixo os comentários:
import { async, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { RouterTestingModule } from '@angular/router/testing';
import { Observable, of, throwError } from 'rxjs';
import { AppComponent } from './app.component';
import { EstadosService } from './services/estados.service';
// Dados que serão retornados pela nossa classe
// de mock (MockEstadosService)
let mockEstadosServiceData = null;
class MockEstadosService {
listar(): Observable<any[]> {
return mockEstadosServiceData;
}
}
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
],
declarations: [
AppComponent,
],
providers: [
// IMPORTANTE
// Aqui dizemos ao Angular que toda vez que EstadosService
// for solicitada, será entregue a classe MockEstadosService
{ provide: EstadosService, useClass: MockEstadosService }
]
}).compileComponents();
}));
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it('deve renderizar a lista de estados', () => {
const fixture = TestBed.createComponent(AppComponent);
// Mock do retorno da nossa service (MockEstadosService)
const dadosMockados = [
{ sigla: 'Te', nome: 'Teste' }
];
// Preenchemos o mockEstadosServiceData para
// que a classe MockEstadosService possa
// retornar o valor ao app.component.ts
//
// O of() retorna um Observable exatamente
// como o httpClient.get
mockEstadosServiceData = of(dadosMockados);
fixture.detectChanges();
const celulasDaTabela = fixture.debugElement
.queryAll(By.css('td'));
// Esperamos encontrar a seguinte estrutura HTML quando
// a página estiver renderizada:
// <table>
// <tr>
// <td>Te</td>
// <td>Teste</td>
// </tr>
// </table>
expect(celulasDaTabela[0].nativeElement.innerHTML)
.toBe(dadosMockados[0].sigla);
expect(celulasDaTabela[1].nativeElement.innerHTML)
.toBe(dadosMockados[0].nome);
});
});
O teste funciona perfeitamente mas ainda precisamos testar um outro comportamento. O que se espera quando a EstadosService
retorna um erro? Será exibida uma mensagem de erro na tela conforme no template app.component.html
, então é justamente isto que iremos testar.
Para não ficar repetindo código, vamos criar mais um it()
dentro código acima:
it('deve renderizar a mensagem de erro', () => {
const fixture = TestBed.createComponent(AppComponent);
// 'Mockamos' um erro, simulando que houve uma falha na
// requisição
mockEstadosServiceData = throwError('teste unitario erro');
fixture.detectChanges();
const divTemplateErro = fixture.debugElement
.query(By.css('div.erro'));
// Esperamos encontrar a seguinte estrutura HTML quando
// a página estiver renderizada:
// <div class="erro">
// Ocorreu um erro inesperado
// </div>
expect(divTemplateErro.nativeElement.innerHTML)
.toMatch(/Ocorreu um erro inesperado/);
});
Resultado do teste unitário:
Observação: estados.service.ts
não é objeto de estudo neste post, portanto ficará sem cobertura de testes
Utilizando o spyOn
Uma outra forma de efetuar o mock da service e isolar o teste do componente é utilizar o spyOn
para mockar as funções da service. Veja no código abaixo o mesmo teste unitário acima porém com algumas modificações:
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { async, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { RouterTestingModule } from '@angular/router/testing';
import { of, throwError } from 'rxjs';
import { AppComponent } from './app.component';
import { EstadosService } from './services/estados.service';
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
// IMPORTANTE
// Como estamos utilizando EstadosService,
// é necessário importar o módulo abaixo
// por conta da dependência HttpClient:
HttpClientTestingModule,
RouterTestingModule,
],
declarations: [
AppComponent,
]
}).compileComponents();
}));
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it('deve renderizar a lista de estados', () => {
const fixture = TestBed.createComponent(AppComponent);
// Mock do retorno da nossa service (MockEstadosService)
const dadosMockados = [
{ sigla: 'Te', nome: 'Teste' }
];
// Pegamos a instância de EstadosService e efetuamos
// o mock no método listar com o spyOn:
const estadosService = TestBed.inject(EstadosService);
spyOn(estadosService, 'listar')
.and
.returnValue(of(dadosMockados));
fixture.detectChanges();
const celulasDaTabela = fixture.debugElement
.queryAll(By.css('td'));
// Esperamos encontrar a seguinte estrutura HTML quando
// a página estiver renderizada:
// <table>
// <tr>
// <td>Te</td>
// <td>Teste</td>
// </tr>
// </table>
expect(celulasDaTabela[0].nativeElement.innerHTML)
.toBe(dadosMockados[0].sigla);
expect(celulasDaTabela[1].nativeElement.innerHTML)
.toBe(dadosMockados[0].nome);
});
it('deve renderizar a mensagem de erro', () => {
const fixture = TestBed.createComponent(AppComponent);
// 'Mockamos' um erro, simulando que houve uma falha na
// requisição
const estadosService = TestBed.inject(EstadosService);
spyOn(estadosService, 'listar')
.and
.returnValue(throwError('teste unitario erro'));
fixture.detectChanges();
const divTemplateErro = fixture.debugElement
.query(By.css('div.erro'));
// Esperamos encontrar a seguinte estrutura HTML quando
// a página estiver renderizada:
// <div class="erro">
// Ocorreu um erro inesperado
// </div>
expect(divTemplateErro.nativeElement.innerHTML)
.toMatch(/Ocorreu um erro inesperado/);
});
});
TestBed.inject
Para quem estava mais acostumado a pegar as instâncias via Dependecy Injector utilizando o comando TestBed.get
, este foi marcado como deprecated e deve-se utilizar em seu lugar o TestBed.inject
a partir da versão 9.
Conclusão
Neste artigo abordamos uma forma de se isolar a unidade de código que esta sendo testada de suas dependências. Em nosso exemplo, efetuamos o mock da classe EstadosService
que é uma dependência do app.component.ts
.
Resultado da cobertura de código:
Não sabe como obter a cobertura do código? dê uma olhada neste outro link: https://consolelog.com.br/cobertura-de-testes-no-angular-utilizando-jest-e-istanbul/
Alguns links interessantes para leitura: