TDD no frontend com Angular.
O TDD ajuda você a prestar atenção às questões certas no momento certo. - Kent Beck
Testar é muito importante. Quando ouvimos sobre o Test Driven Development, associamos sempre ao backend, mas é possível também aplicar ao frontend. O objetivo do artigo é ajudar aqueles que desejam praticar esta técnica concebida por Kent Beck em suas aplicações. Para que este artigo não fique grande, não vou explicar o que alguns termos significam, poderá ler mais sobre TDD no artigo com o tema, o que é tdd e bdd?. Vamos focar na prática ao decorrer deste artigo, com um exemplo mão na massa de como realmente funciona o TDD. O exemplo é simples, mas para fins didáticos funciona muito bem!
Testando Serviços
Vamos iniciar o desenvolvimento orientado a testes em nossas services. Mas não vamos realizar requisições HTTP. Neste artigo vamos focar na lógica de negócios. No caso imagine que precisamos criar uma lógica que incrementa e decrementa valores. Então temos algumas regras:
- O valor inicial do contador deve ser zero.
- Deve permitir incrementar (adicionar).
- Deve permitir decrementar (remover/diminuir).
- Não é permitido ter itens menores que zero (número negativos não são permitidos).
Agora que definimos nossos passos, vamos iniciar com o teste, sem nada criado na service :
// exemplo.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { ContadorService } from './contador.service';
describe('ContadorService', () => {
let service: ContadorService;
ContadorService
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(ContadorService);
});
/* Teste que garante que a classe foi criada corretamente. */
it('should be created', () => {
expect(service).toBeTruthy();
});
/* O valor inicial do contador deve ser zero. */
it('deve ter a propriedade contador com valor inicial 0', () => {
expect(service.contador).toBe(0);
})
});
Na classe contador, precisamos sempre garantir que a propriedade se inicie em 0. Por isso passamos expect(service.contador).toBe(0);
. Se rodar o teste deve falhar e isso é importante para a prática do TDD. Para rodar o teste em um projeto recém criado, basta executar no terminal o comando npm start. Repare que vai receber uma falha no teste e no arquivo, pois não temos nada criado nem mesmo a propriedade:
Feito isso podemos adicionar a propriedade em nossa classe:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class ContadorService {
public contador: number = 0;
constructor() { }
}
Ao executar novamente os testes vai reparar que tivemos sucesso para essa condição do teste:
Vamos avançar e a segunda regra é:
- Deve permitir incrementar (adicionar).
Certo então precisamos de uma lógica para isso, mas seguindo o TDD garantimos que os testes sejam criados primeiro. Então vamos para o arquivo spec.ts
:
import { TestBed } from '@angular/core/testing';
import { ContadorService } from './contador.service';
describe('ContadorService', () => {
let service: ContadorService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(ContadorService);
});
it('deve ser criado a classe ContadorService', () => {
expect(service).toBeTruthy();
});
/* O valor inicial do contador deve ser zero. */
it('deve ter a propriedade contador com valor inicial 0', () => {
expect(service.contador).toBe(0);
})
/* Deve permitir incrementar (adicionar). */
it('deve incrementar a contagem', () => {
expect(service.contador).toBe(0);
service.adicionarContagem();
expect(service.contador).toBe(1);
});
});
Os testes devem novamente falhar e apontar em vermelho o erro. Vamos para a lógica de incrementar na service
:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class ContadorService {
public contador: number = 0;
constructor() { }
public adicionarContagem() {
this.contador++;
}
}
O resultado deve ser sucesso já que estamos atendendo as condições do teste:
Agora vamos mudar um pequeno detalhe. Vamos adicionar na classe de serviço apenas o método de diminuirContagem sem lógica:
public diminuirContagem() { }
Então agora voltamos nos testes e pensamos na lógica. Já que precisamos diminuir os números, podemos fazer dessa maneira:
it('deve diminuir a contagem', () => {
service.contador = 2;
service.diminuirContagem();
expect(service.contador).toBe(1);
});
Execute o teste e ele deve falhar mostrando agora uma mensagem de erro específica para este teste:
Certo estamos avançando. Crie a lógica para atender a necessidade de cobrir este teste:
public diminuirContagem() {
this.contador--;
}
Execute novamente os testes e vamos ter sucesso:
Finalizamos? Não! Ainda falta um detalhe importante. A nossa regra, que foi definida antes de iniciarmos o desenvolvimento orientado a testes, não permite que seja possível diminuir para valores menos que 0, não implementamos testes nem validações para evitar isso. Então temos que criar esse teste também:
it('não deve diminuir quando a contagem é 0', () => {
expect(service.contador).toBe(0); // inicia o teste em 0
service.diminuirContagem(); // chama a função que diminui o valor para -1
expect(service.contador).toBe(0); // agora o contador recebe -1, mas é esperado 0
});
Essa é uma maneira simples de garantir que a propriedade contador nunca caia abaixo de 0. Ao rodar os testes devem falhar:
O erro aponta que o expect recebeu -1 mas deveria ser 0. No nosso caso é porque estamos desenvolvendo primeiro os testes. Vamos criar a lógica. Mas pense sempre que devemos passar da maneira mais simples possível o teste para depois se necessário refatorar:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class ContadorService {
public contador: number = 0;
constructor() { }
public adicionarContagem() {
this.contador++;
}
public diminuirContagem() {
if (this.contador === 0) return;
this.contador--;
}
}
/* contador.spec.ts*/
import { TestBed } from '@angular/core/testing';
import { ContadorService } from './contador.service';
describe('ContadorService', () => {
let service: ContadorService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(ContadorService);
});
it('deve ser criado a classe ContadorService', () => {
expect(service).toBeTruthy();
});
it('deve ter a propriedade contador com valor inicial 0', () => {
expect(service.contador).toBe(0);
})
it('deve incrementar a contagem', () => {
expect(service.contador).toBe(0);
service.adicionarContagem();
expect(service.contador).toBe(1);
});
it('deve diminuir a contagem', () => {
service.contador = 2;
service.diminuirContagem();
expect(service.contador).toBe(1);
});
it('deve impedir diminuir quando a contagem está no 0', () => {
expect(service.contador).toBe(0);
service.diminuirContagem();
expect(service.contador).toBe(0);
});
});
Agora sim nossos testes passam com sucesso e finalizamos essa parte de criar a lógica no serviço da nossa aplicação do lado do frontend:
E esse passo a passo foi muito bem descrito por Kent Beck:
O TDD ajuda você a prestar atenção às questões certas no momento certo, para que você possa tornar seus projetos mais limpos e refinar seus projetos à medida que aprende. - Kent Beck, Desenvolvimento Orientado a Testes: Com Exemplo
Conclusão
O que vimos ao decorrer deste artigo foi uma simples demonstração de como colocar o Test Driven Development em prática. Ao iniciar na criação dos testes o desenvolvimento, pensamos com clareza no que a função deve fazer e em todos os cenários que devemos testar. Isso nos auxilia muito a ser objetivos no desenvolvimento de funcionalidades. Espero que este conteúdo tenha sido útil e qualquer crítica construtiva fiquem à vontade para deixar nos comentários! Até o próximo post!