Angular - Testes de Pipes e Services
Wesley Egberto
Posted on May 15, 2021
No post anterior falamos sobre o básico de testes, neste iremos iniciar os testes unitários das estruturas do Angular.
Outros posts da série:
- Parte 1 - Introdução aos Testes
- Parte 2 - Testes de Pipes e Services
- Parte 3 - Testes de Componentes
Estruturas que iremos testar:
- Pipes
- Services
Pipes
Como mostrado no post anterior os testes de Pipes não tem segredo.
Podemos suprir as dependências da classe utilizando mocks ou instâncias manuais - recomendo usar mock de modo que possa a testar de fato apenas as regras da pipe.
Sempre que possível, é interessante testar todas as possíveis variações dos valores de entrada (limites do domínio de entrada) para que não tenhamos nenhum caso não tratato, pois não saberemos quem poderá utilizar nossa Pipe.
Dado a pipe abaixo no arquivo strength.pipe.ts
:
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'strength' })
export class StrengthPipe implements PipeTransform {
transform(value: number): string {
if (value < 0) {
return '(invalid)';
} else if (value < 10) {
return `${value} (weak)`;
} else if (value >= 10 && value < 20) {
return `${value} (strong)`;
} else {
return `${value} (unbelievable)`;
}
}
}
Seguindo o padrão do Angular, criaremos o arquivo de teste strength.pipe.spec.ts
:
import { StrengthPipe } from './strength.pipe';
describe('Pipe: Strength', () => {
let pipe: StrengthPipe;
beforeEach(() => {
pipe = new StrengthPipe();
});
it('should display the strengh with sufix `weak` when between 0 and 10', () => {
expect(pipe.transform(0)).toBe('0 (weak)');
expect(pipe.transform(5)).toBe('5 (weak)');
expect(pipe.transform(9)).toBe('9 (weak)');
});
it('should display the strengh with sufix `strong` when between 10 and 20', () => {
expect(pipe.transform(10)).toBe('10 (strong)');
expect(pipe.transform(15)).toBe('15 (strong)');
expect(pipe.transform(19)).toBe('19 (strong)');
});
it('should display the strengh with sufix `unbelievable` when >= 20', () => {
expect(pipe.transform(20)).toBe('20 (unbelievable)');
expect(pipe.transform(57)).toBe('57 (unbelievable)');
expect(pipe.transform(100)).toBe('100 (unbelievable)');
});
it('should display the invalid when < 0', () => {
expect(pipe.transform(-1)).toBe('(invalid)');
});
});
Services
Os testes de services tem uma estrutura similar aos testes de pipes, o que pode complicar os casos de testes são as dependências do service.
Podemos ter uma service que apenas gerencia estado, executa alguma lógica de negócio ou que realiza uma requisição HTTP para uma API.
Caso nossa service utilize alguma lib é recomendado verificar se tal lib tem uma versão de testes que nos ajude a mockar ou simular seu comportamento, permitindo assim uma melhor qualidade no teste.
Para as services normalmente podemos ter:
- testes isolados: casos onde não existe dependência ou casos em que criamos mock de todas elas;
- testes integrados: casos onde testamos o comportamento da nossa service junto com as suas dependências.
Service Sem Dependências
Abaixo segue um exemplo de uma service que gerencia o estado em um array.
Arquivo message.service.ts
:
import { Injectable } from '@angular/core';
@Injectable()
export class MessageService {
messages: string[] = [];
add(message: string) {
this.messages.push(message);
}
clear() {
this.messages = [];
}
}
Arquivo de teste message.service.spec.ts
:
import { MessageService } from './message.service';
describe('MessageService', () => {
// cut (class under testing) - cute para não pegarmos aversão aos testes =)
let cut: MessageService = null;
beforeEach(() => {
cut = new MessageService();
});
it('should have no message when starting', () => {
expect(cut.messages.length).toBe(0);
});
it('should add a message and keep it', () => {
cut.add('message1');
expect(cut.messages.length).toBe(1);
const storedMessage = cut.messages[0];
expect(storedMessage).toBe('message1');
});
it('should remove all messages when clear is called', () => {
cut.add('message to remove');
cut.clear();
expect(cut.messages.length).toBe(0);
});
});
Service com Dependência
Para criar um teste unitário de uma service que tem uma dependência qualquer podemos utilizar o Jasmine para nos ajudar a criar o mock dessa dependência de forma que nos permita mockar seu retorno ou verificar se um método foi chamado.
Como exemplo, tomemos o service abaixo que depende de uma classe LogService
que tenha um método log(string): boolean
e que sempre precisa logar o resultado do cálculo:
@Injectable()
export class CalculationService {
constructor(private readonly logger: LogService) {}
calculate(): number {
const answer = 42; // magic
const logged = this.logger.log(`Calculation result: ${answer}`);
// alguma regra que obrigue sempre a logar o resultado
if (!logged)
throw new Error("Result was not logged");
return answer;
}
}
Podemos criar o teste calculation.service.spec.ts
abaixo:
describe('CalculationService', () => {
let mockedLogger: LogService;
let service: CalculationService;
beforeEach(() => {
// criamos o mock da nossa dependência passando os métodos logados
mockedLogger = jasmine.createSpyObj(['log']);
// instanciamos nossa service passando a sua dependência
service = new CalculationService(mockMessageService);
});
describe('calculate', () => {
it('should return 42 and log it', () => {
// mockamos o retorno do método que dependemos (informa que logou)
mockMessageService.log.and.returnValue(true);
const result = service.calculate();
// validamos o retorno da service
expect(result).toBe(42);
// verificamos se o método foi chamado
expect(mockedLogger.log).toHaveBeenCalledTimes(1);
// também podemos verificar se foi passado o argumento esperado
expect(mockMessageService.add).toHaveBeenCalledWith('Calculation result: 42');
});
// caso onde testamos o comportamento caso não seja logado
it('should throw error if result was not logged', () => {
// mockamos o retorno informando que não logou
mockMessageService.log.and.returnValue(false);
// podemos também testar o lançamento de erros
expect(() => service.calculate()).toThrow(new Error("Result was not logged"));
});
});
});
Service com HTTP Client
Para testar uma service que faz uma requisição HTTP podemos utilizar um módulo de teste HttpClientTestingModule que o Angular disponibiliza.
Para os testes com requisição HTTP precisamos configurar e inicializar o módulo de teste do Angular de modo que ele configure um mock do HttpClient que faça uma requisição falsa ao capturar a URL que foi chamada e retorne um response que precisamos para testar o comportamento da service.
Para configurar o módulo de testes utilizaremos a classe TestBed
que o Angular fornece para facilitar a configuração do teste.
Essa configuração precisa ser feita antes dos nossos casos de testes, para isso utilizaremos o método beforeEach
que é invocado antes de cada teste.
Ainda no método beforeEach
, precisamos recuperar uma instância da classe HttpTestingController
que o Angular configura para nós, com ela iremos poder validar quais URLs foram chamadas, quantoas vezes e forçar um retorno simulado.
Com a service hero.service.ts
:
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { Hero } from './hero';
const httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};
@Injectable()
export class HeroService {
constructor(private http: HttpClient) { }
// a regra da nossa service é adicionar um simples prefixo no nome
getHeroes(): Observable<any> {
return this.http.get('/api/heroes')
.pipe(tap(heroes => {
if (heroes && heroes.length) {
heroes.forEach(hero => {
hero.name = `Super ${hero.name}`;
});
}
}));
}
saveHero(hero: any): Observable<Hero> {
return this.http.post('/api/heroes', hero, httpOptions);
}
Podemos criar um caso de teste hero.service.spec.ts
:
import { TestBed, inject } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { HeroService } from './hero.service';
import { MessageService } from './message.service';
describe('HeroService', () => {
let httpTestingController: HttpTestingController;
let heroSvc: HeroService;
beforeEach(() => {
// no `providers` precisamos adicionar todas as dependências da nossa service
TestBed.configureTestingModule({
imports: [ HttpClientTestingModule ],
providers: [ HeroService ]
});
// recuperamos a instância do controle de teste
httpTestingController = TestBed.get(HttpTestingController);
// recuperamos nossa service já configurada com sua dependência (HttpClient)
heroSvc = TestBed.get(HeroService);
});
// aqui garantimos que nossa service e suas dependências conseguem ser criadas
it('should be able to create a service', inject([HeroService], (heroService: HeroService) => {
expect(heroService).toBeDefined();
}));
// caso nosso método tenha vários testes é interessante agrupar em um `describe` para facilitar a visualização no relatório
describe('getHeroes', () => {
// caso onde verificamos se a URL correta foi chamada
it('should call get with the correct URL', () => {
// aqui nosso subscriber não fará nada visto que estamos interessando em testar a requisição
heroSvc.getHeroes().subscribe();
// podemos verificar a requisição já validando se foi feita para determinada URL
httpTestingController.expectOne('/api/heroes');
// verifica as expectativas configuradas no controle de teste
httpTestingController.verify();
});
// case onde verificamos o retorno da service
it('should update the name with Super prefix', () => {
heroSvc.getHeroes()
// aqui podemos utilizar o subscriber para verificar o resultado
.subscribe(heroes => {
expect(heroes).toBeTruthy();
expect(heroes.length).toBe(1);
const hero = heroes[0];
expect(hero.name).toBe('Super Odair');
expect(hero.strength).toBe(42);
});
// `expectOne` captura a requisição e retorna um `TestRequest` onde podemos configurar a resposta da requisição
const req = httpTestingController.expectOne('/api/heroes');
// aqui setamos o JSON de retorno da URL
req.flush([ { id: 42, name: 'Odair', strength: 42 } ]);
// rodamos nossa verificação
httpTestingController.verify();
});
});
// caso de teste para salvar
describe('saveHero', () => {
it('should save a hero', () => {
heroSvc.saveHero({ name: 'Mr. Anderson', strength: 42 })
.subscribe(hero => {
expect(hero).toBeTruthy();
// verificamos se a service não alterou o resultado da API
expect(hero.name).toBe('Neo');
expect(hero.strength).toBe(42);
// validamos o ID gerado
expect(hero.id).toBe(1)
});
const req = httpTestingController.expectOne('/api/heroes');
// validamos o JSON da requisição para verificar se service não alterou o que a tela passou
expect(req.request.body.name).toBe('Mr. Anderson');
expect(req.request.body.strength).toBe(42);
// mockamos o retorno criado pela API para rodar o subscriber
// retornamos outro nome para verificar se a service retornou o response da API =)
req.flush({ id: 1, name: 'Neo', strength: 42 });
httpTestingController.verify();
});
});
});
A classe de teste estruturada acima irá gerar uma seção no relatório do Karma abaixo:
No próximo post criaremos alguns testes de componentes de tela.
Posted on May 15, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.