Como mockar uma service ou dependência de um componente - Angular + JEST
Isolar a unidade de código de suas dependências é extremamente importante para a construção de testes unitários. Neste post é abordado como isolar estas dependências de um componente através do dependency injector ou spyOn com foco nos testes unitários.
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: