TDD no frontend com Angular.

O TDD ajuda você a prestar atenção às questões certas no momento certo. - Kent Beck

TDD no frontend com Angular.

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:

image.png

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:

image.png

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:

image.png

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:

image.png

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:

image.png

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:

image.png

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:

image.png

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!