Novembro de 2018

Volume 33 - Número 11

O Programador Profissional - Como ser MEAN: Teste Angular

Por Ted Neward | Novembro de 2018

Ted NewardBem-vindos de volta, MEANers.

Com um pouco de sorte, o “debate” em torno do teste de unidade do código não é mais necessário — como desenvolvedor, não há dúvida que você deve procurar maneiras de automatizar o código de teste que escreve. Os argumentos podem continuar de forma que os testes fiquem antes ou depois do código em questão, mas fica muito claro que os testes não são mais uma parte opcional de um projeto de software moderno. Isso, é claro, levanta a questão: Como você pode testar um aplicativo Angular? Falei brevemente nos arquivos de teste, novamente sobre o problema ocorrido em maio de 2017, quando comecei a criar alguns componentes Angulares, mas esse assunto não é muito extenso (msdn.com/magazine/mt808505). Agora é hora de uma análise mais profunda.

Voltar ao início

Vamos voltar uma etapa até o início do aplicativo. Quando "ng new" é executado para adicionar scaffold aos estágios iniciais do aplicativo, ele cria todas as ferramentas e hooks e arquivos necessários para garantir que o aplicativo possa ser testado. Na verdade, imediatamente após "ng new", sem fazer nem mesmo uma única alteração em qualquer arquivo, você poderá executar "ng test" para executar o executor de teste que, por sua vez, executa o código escrito em relação ao código da estrutura de teste gerado por scaffolding.

Quando executado, "npm test" dispara o Karma, o executor de teste, que, em seguida, dispara uma instância do navegador e executa os testes dentro dessa instância. O Karma continua em execução, mantendo um WebSocket aberto para o navegador, de forma que todas as alterações feitas nos arquivos de origem possam ser testadas imediatamente, removendo a parte da sobrecarga de teste e nos trazendo mais para perto de um ciclo de codificação e teste sem espera. O scaffolding do projeto fornece três testes, codificados no arquivo "app.component.spec.ts", que testa o código "app.component.ts" correspondente. Como regra geral, cada componente no aplicativo Angular deve ter um arquivo ".spec.ts" correspondente para conter todos os testes. Se cada componente for criado pela CLI do Angular, ele normalmente irá gerar um arquivo de teste correspondente, com algumas exceções (por exemplo, "class") que exigirá um argumento "-spec" para o comando "generate" criar o arquivo spec (abreviação de "especificação").

Na verdade, vamos gerar uma classe rápida, Speaker (obviamente) e, em seguida, escrever alguns testes para ele. Como de costume, crie o componente com a CLI do Angular (“ng generate class Speaker --spec”) e edite o arquivo "speaker.ts" para conter uma classe simples com cinco propriedades públicas do construtor:

export class Speaker {
  constructor(public id: number,
    public firstName: string,
    public lastName: string,
    public age: number,
    public bio?: string,
    )
  { }
}

Os testes correspondentes devem exercitar as várias propriedades para garantir que elas funcionem conforme o esperado:

import { Speaker } from './speaker';
describe('Speaker', () => {
  it('should create an instance', () => {
    expect(new Speaker(1, "Ted", "Neward", 47, "Ted is a big geek")).toBeTruthy();
  });
  it('should keep data handy', () => {
    var s = new Speaker(1, "Ted", "Neward", 47, "Ted is a big geek");
    expect(s.firstName).toBe("Ted");
    expect(s.firstName).toEqual("Neward");
    expect(s.age).toBeGreaterThan(40);   
  })
});

À medida que a classe fica mais complicada, testes mais complexos também devem ser adicionados. Muito da potência do teste reside na estrutura “expect”, que fornece um grande número de métodos “toBe” para testar vários cenários. Aqui você vê vários tipos, incluindo “toEqual”, que faz um teste de igualdade e “toBeGreaterThan”, que faz exatamente o que o nome sugere.

Mas aqueles que estão acompanhando em casa, no entanto, verão que algo está errado — depois que o arquivo de especificações é salvo, a instância do navegador aberta obtém um horrendo tom de vermelho, destacando que “Ted” não é igual a “Neward”! Claro, há um bug na segunda instrução “expect”, querendo comparar “firstName”, quando deveria ser “lastName”. Isso é bom, pois embora o código de teste esteja procurando bugs no código, também é o caso de às vezes o bug estar no teste, e obter esse feedback assim que você escreve o teste ajuda a evitar bugs de teste.

Mais testes

Obviamente, um aplicativo Angular é composto por classes mais simples. Além disso, também pode incluir serviços, que geralmente são muito simples de testar, já que tendem a fornecer o comportamento e muito pouco estado. No caso de serviços que oferecem algum comportamento próprio, como formatação ou alguma transformação de dados simples, os testes são fáceis e reminiscentes de testes de uma classe como Speaker. Mas os serviços também costumam ter que interagir com o mundo ao redor deles de alguma forma (como fazer solicitações HTTP, como o SpeakerService fez há algumas colunas), o que significa que testá-los ficará mais complicado se você não quiser incluir as dependências. Na verdade, o envio de solicitações via HTTP, por exemplo, tornaria os testes sujeitos aos caprichos da comunicação de rede ou das interrupções de servidor, o que poderia produzir algumas falhas de falsos negativos e tornar os testes menos determinísticos. Isso seria ruim.

É para essas situações que o Angular faz tal uso intenso de injeção de dependência.

Por exemplo, vamos começar pela versão do SpeakerService que não fez quaisquer solicitações HTTP, conforme mostrado na Figura 1.

Figura 1 A classe SpeakerService que não fez quaisquer solicitações HTTP

@Injectable()
export class SpeakerService {
  private static speakersList : Speaker[] = [
    new Speaker(1, "Ted", "Neward", 47,
      "Ted is a big geek living in Redmond, WA"),
    new Speaker(2, "Brian", "Randell", 47,
      "Brian is a high-profile speaker and developer of 20-plus years.
      He lives in Southern California."),
    new Speaker(3, "Rachel", "Appel", 39,
      "Rachel is known for shenanigans the world over. She works for Microsoft."),
    new Speaker(4, "Deborah", "Kurata", 39,
      "Deborah is a Microsoft MVP and Google Developer Expert in Angular,
      and works for the Google Angular team."),
    new Speaker(5, "Beth", "Massi", 39,
      "Beth single-handedly rescued FoxPro from utter obscurity
      and currently works for Microsoft on the .NET Foundation.")
  ]
  public getSpeakers() : Speaker[] { return SpeakerService.speakersList; }
  public getSpeakerById(id : number) : Speaker {
    return SpeakerService.speakersList.find( (s) => s.id == id);
  }
}

Essa versão é simples de ser testada, porque é síncrona e não exige dependências externas, como mostrado na Figura 2.

Figura 2 Teste da classe SpeakerService

describe('SpeakerService', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [SpeakerService]
    });
  });
  it('should be able to inject the SpeakerService', inject([SpeakerService],
    (service: SpeakerService) => {
    expect(service).toBeTruthy();
  }));
  it('should be able to get a list of Speakers', inject([SpeakerService],
    (service: SpeakerService) => {
    expect(service.getSpeakers()).toBeDefined();
    expect(service.getSpeakers().length).toBeGreaterThan(0);
  }));
});

Notou as chamadas de “inject” em cada teste? Isso é basicamente como o Angular gerencia a injeção de dependência no ambiente de teste; é o que permite que você forneça qualquer tipo de back-end de serviço compatível (real ou fictício) para o ambiente.

Normalmente, o serviço faz um pouco mais do que o SpeakerService simples, e é muito chato ter que comentar e/ou substituir o “real” por um falso que não faz nada, portanto, é aqui que o uso de um serviço “fictício” funciona melhor. O Angular tem um constructo útil chamado “Spy”, que pode inserir a si próprio em um serviço regular e substituir determinados métodos para fornecer um resultado fictício:

it('should be able to get a list of Speakers',
    inject([SpeakerService], (service: SpeakerService) => {
  spy = spyOn(service, 'getSpeakers').and.returnValues([]);
  var speakers: Speaker[] = service.getSpeakers();
  expect(speakers).toBeDefined();
  expect(speakers.length).toBe(0);
}));

Usando o Spy, você pode “substituir” o método que está sendo invocado no teste a fim de fornecer valores que você deseja retornar.

Testes de componentes

No entanto, uma grande parte da criação de um aplicativo Angular consiste em criar componentes que podem aparecer na página, e é importante poder testá-los também. Para obter uma compreensão ainda melhor sobre o teste de um componente visual, vamos começar com um simples componente de tipo de botão de alternância ativar/desativar:

@Component({
  selector: 'app-toggle',
  template: `<button (click)="clicked()">
    I am {{isOn ? "on" : "off" }} -- Click me!
  </button>`,
  styleUrls: ['./toggle.component.css']
})
export class ToggleComponent {
  public isOn = false;
  public clicked() { this.isOn = !this.isOn; }
}

Para testar isso, você pode literalmente ignorar o DOM inteiro e apenas examinar o estado do componente quando várias ações são invocadas:

it('should toggle off to on and off again', () => {
  const comp = new ToggleComponent();
  expect(comp.isOn).toBeFalsy();
  comp.clicked();
  expect(comp.isOn).toBeTruthy();
  comp.clicked();
  expect(comp.isOn).toBeFalsy();
});

No entanto, isso não verifica o DOM, o que pode ocultar alguns bugs críticos. Só porque a propriedade “isOn” foi alterada, isso não significa que o modelo renderizou a propriedade corretamente, por exemplo. Para verificar isso, você pode obter a instância do componente criada pelo acessório e examinar o DOM renderizado para ele, da seguinte forma:

it ('should render HTML correctly when clicked', () => {
  expect(fixture.componentInstance.isOn).toBeFalsy();
  const b = fixture.nativeElement.querySelector("button");
  expect(b.textContent.trim()).toEqual("Click me! I am OFF");
  fixture.componentInstance.clicked();
  fixture.detectChanges();
  expect(fixture.componentInstance.isOn).toBeTruthy();
  const b2 = fixture.nativeElement.querySelector("button");
  expect(b2.textContent.trim()).toEqual("Click me! I am ON");
});

O “nativeElement” aqui obtém o nó DOM para o componente e posso usar “querySelector” para fazer uma consulta no estilo jQuery para localizar o nó DOM interno relevante — neste caso, o botão criado por Toggle. A partir daí, pego o conteúdo de texto dele (e o corto, porque a linha de código de demonstração anterior é interrompida em dois lugares onde seria impraticável replicar no teste) e o comparo aos resultados esperados. No entanto, observe que depois que eu “clico” no componente, há uma chamada para “detectChanges”. Isso ocorre porque preciso dizer ao Angular para processar as alterações relativas ao DOM que podem ter sido causadas pelo manipulador de eventos, como a atualização das cadeias de caracteres interpoladas no modelo. Sem isso, os testes vão falhar, apesar de o componente estar funcionando perfeitamente no navegador. (Eu cometi esse mesmo erro enquanto escrevia o artigo, portanto, não se aflija se você se esquecer de detectar as alterações). Observe que, se o componente fizer alguma inicialização significativa dentro de seu método onInit, o teste precisará de detectChanges antes de fazer qualquer trabalho significativo, pelo mesmo motivo.

Conclusão

A propósito, tenha em mente que tudo o que esse teste codifica se refere ao lado do cliente do aplicativo, e não ao lado do servidor. Você se lembra de todo o código do Express que escrevi para fornecer APIs para armazenar dados no banco de dados e assim por diante? Tudo isso fica basicamente “fora” dessa estrutura e, portanto, precisa ser mantido e executado separadamente. Você pode usar algumas das ferramentas de “compilação” que abordei para executar os testes do lado do servidor e do lado do cliente como parte de um ciclo de testes e, assim, certificar-se de que eles são disparados como parte de todas as alterações no cliente ou no servidor. O Angular também dá suporte ao teste “E2E” (abreviação de “ponta a ponta”), que está fora do escopo do que está sendo falado aqui, mas se destina a dar suporte a exatamente essa situação.

Boa codificação.


Ted Neward é consultor de politecnologia, palestrante e mentor baseado em Seattle, e trabalha atualmente como diretor de engenharia e relações com desenvolvedores na Smartsheet.com. Ele já escreveu uma enormidade de artigos, é autor e coautor de dezenas de livros e faz palestras no mundo inteiro. Entre em contato com ele em ted@tedneward.com ou leia seu blog em blogs.tedneward.com.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Garvice Eakins (Smartsheet.com)


Discuta esse artigo no fórum do MSDN Magazine