DEV Community

Cover image for Angular: Teste Unitário com Spectator
Rogerio Fonseca
Rogerio Fonseca

Posted on • Updated on

Angular: Teste Unitário com Spectator

Introdução ao Tema

Neste post vou te mostrar um exemplo de como podemos utilizar o Spectator para criar Mocks ou Stubs e construir os cenários de teste, simulando uma request HTTP de uma API externa em um sistema que estamos desenvolvendo.

Neste exemplo usei a VIACEP como exemplo para ser consumido.

Vá direto ao ponto

Apresentação do Problema

Ao implementar um cenário de teste que necessita fazer uma requisição à um serviço externo precisamos construir uma resposta falsa (fake) para suprir a resposta de um serviço externo que não estará disponível no momento do teste unitário.

Para este exemplo irei utilizar como exemplo o servico de API ViaCEP

Configuração

Para adicionar a dependência do spectator

npm install @ngneat/spectator --save-dev
Enter fullscreen mode Exit fullscreen mode

Exemplo de retorno da Request

Para começar, se quiser ter uma ideia de como será o retorno da requisição que iremos trabalhar basta executar o comando abaixo no terminal para verificar o retorno do nosso exemplo.

curl -X GET http://viacep.com.br/ws/38660000/json/
Enter fullscreen mode Exit fullscreen mode

Modelo de resposta da requisição

O resultado da execução será um modelo JSON como este:

{
  "cep": "38660-000",
  "logradouro": "",
  "complemento": "",
  "bairro": "",
  "localidade": "Buritis",
  "uf": "MG",
  "ibge": "3109303",
  "gia": "",
  "ddd": "38",
  "siafi": "4185"
}
Enter fullscreen mode Exit fullscreen mode

Construindo Cenários e Declarando Dependências

Atenção!!

Não se preocupe em copiar os códigos inicialmente pois mostrarei um exemplo mais completo no final..

O primeiro passo para a construção do cenário é suprir a estrutura com todas as dependência da classe que será testada.

No nosso exemplo temos uma dependência com "FormsModule" e outra dependência com "ListCepAPI" que é uma dependência indireta através do ListCepService.

  const createComponent = createComponentFactory({
    component: ListCepComponent,
    imports: [FormsModule],
    mocks: [
      ListCepAPI,
    ],
    detectChanges: false
  });

  const createService = createServiceFactory({
    service: ListCepService,
  });
Enter fullscreen mode Exit fullscreen mode

Definição de Mock ou de retorno Fake

Nesta etapa iremos definir qual será o retorno que o servidor devolveria em um caso real. Como nesta etapa não teremos uma infraestrutura disponível iremos devolver um resultados "fabricado".

  beforeEach(() => {
    spectatorComponent = createComponent();
    spectatorService = createService();
    component = spectatorComponent.component;
    service = spectatorComponent.inject<ListCepService>(ListCepService);
    apiMocked = spectatorService.inject<ListCepAPI>(ListCepAPI);
    apiMocked.findAddress.andReturn(Promise.resolve(fakeResponse));
  });
Enter fullscreen mode Exit fullscreen mode

Construção da resposta Fake

Observe que foi construído um objeto expectData que será utilizado para verificação do resultado e outro objeto fake chamado "fakeResponse" para ser devolvido em

apiMocked.findAddress.andReturn(Promise.resolve(fakeResponse)
Enter fullscreen mode Exit fullscreen mode
  // Fake Object
  const fakeResponse: Address = {
    cep: '01001-000',
    logradouro: 'Praça da Sé',
    complemento: 'lado ímpar',
    bairro: '',
    localidade: 'São Paulo',
    uf: 'SP',
    ibge: '3550308',
    gia: '1004',
    ddd: '11',
    siafi: '7107'
  };

  // Dados Esperados
  const expectData: Address = {
    cep: '01001-000',
    logradouro: 'Praça da Sé',
    complemento: 'lado ímpar',
    bairro: '',
    localidade: 'São Paulo',
    uf: 'SP',
    ibge: '3550308',
    gia: '1004',
    ddd: '11',
    siafi: '7107',
    enderecoCompleto: 'Praça da Sé, Sé, São Paulo'
  };
Enter fullscreen mode Exit fullscreen mode

Validação de regras de negócios

Um exemplo de uma regra de negócios seria o campo de enderecoCompleto que não existe no retorno da API porém ocorre uma transformação dos dados recebidos para construir este campo. Neste caso, por exemplo, o campo poderia ser um calculo de frete ou qualquer outro tipo de transformação dos dados recebidos através da chamada ao serviço externo.

Verificações

Após a construção do cenário nosso foco deverá ser na construção das nossas verificações ou asserções.

🚧 🚨 Atenção para o exemplo do assert 'should check service result' para este caso resolvi deixar um console.log() apenas para que veja um exemplo de como será o resultado. Porém, ao enviar para produção os testes NÃO deverão conter comandos de display.

  it('should exist component', () => {
    expect(component).toBeTruthy();
  });

  it('should exist service', () => {
    expect(service).toBeTruthy();
  });

  it('should exist apiMocked', () => {
    expect(apiMocked).toBeTruthy();
  });

  it('should check service result', async () => {
    const result = await service.getAddress();
    console.log(result);

    expect(dataExpect).toEqual(result);
  });

  it('should click button', async () => {
    spectatorComponent.click('#searchAddress');
    spectatorComponent.detectChanges();
    const enderecoCompleto = spectatorComponent.query('#enderecoCompleto').textContent;

    const enderecoCompletoExpected = 'Endereço completo: Praça da Sé, Sé, São Paulo'
    expect(enderecoCompletoExpected).toEqual(enderecoCompleto);
  });
Enter fullscreen mode Exit fullscreen mode

Exemplo completo:

📄 Link para arquivo no Github

import { FormsModule } from '@angular/forms';
import { SpyObject } from '@ngneat/spectator';
import {
  Spectator,
  createComponentFactory,
  createServiceFactory,
  SpectatorService,
} from '@ngneat/spectator/jest';
// quem estiver executando os tetes apenas com o Karma.js o jest deverá ser removido do caminho
//} from '@ngneat/spectator/';
import { Address } from './address.model';
import { ListCepAPI } from './list-cep.api';
import { ListCepComponent } from './list-cep.component';

import { ListCepService } from './list-cep.service';

describe('ListCepComponent', () => {
  const createComponent = createComponentFactory({
    component: ListCepComponent,
    imports: [FormsModule],
    mocks: [
      ListCepAPI,
    ],
    detectChanges: false
  });

  const createService = createServiceFactory({
    service: ListCepService,
  });

  let spectatorComponent: Spectator<ListCepComponent>;
  let spectatorService: SpectatorService<ListCepService>;
  let component: ListCepComponent;

  let service: SpyObject<ListCepService>;
  let apiMocked: SpyObject<ListCepAPI>;

  beforeEach(() => {
    spectatorComponent = createComponent();
    spectatorService = createService();
    component = spectatorComponent.component;
    service = spectatorComponent.inject<ListCepService>(ListCepService);
    apiMocked = spectatorService.inject<ListCepAPI>(ListCepAPI);
    apiMocked.findAddress.andReturn(Promise.resolve(fakeResponse));
  });

  it('should exist component', () => {
    expect(component).toBeTruthy();
  });

  it('should exist service', () => {
    expect(service).toBeTruthy();
  });

  it('should exist apiMocked', () => {
    expect(apiMocked).toBeTruthy();
  });

  it('should check service result', async () => {
    const result = await service.getAddress();
    console.log(result);

    expect(dataExpect).toEqual(result);
  });

  it('should click button', async () => {
    spectatorComponent.click('#searchAddress');
    spectatorComponent.detectChanges();
    const enderecoCompleto = spectatorComponent.query('#enderecoCompleto').textContent;

    const enderecoCompletoExpected = 'Endereço completo: Praça da Sé, Sé, São Paulo'
    expect(enderecoCompletoExpected).toEqual(enderecoCompleto);
  });

  const fakeResponse: Address = {
    cep: '01001-000',
    logradouro: 'Praça da Sé',
    complemento: 'lado ímpar',
    bairro: '',
    localidade: 'São Paulo',
    uf: 'SP',
    ibge: '3550308',
    gia: '1004',
    ddd: '11',
    siafi: '7107'
  };

  const dataExpect: Address = {
    cep: '01001-000',
    logradouro: 'Praça da Sé',
    complemento: 'lado ímpar',
    bairro: '',
    localidade: 'São Paulo',
    uf: 'SP',
    ibge: '3550308',
    gia: '1004',
    ddd: '11',
    siafi: '7107',
    enderecoCompleto: 'Praça da Sé, Sé, São Paulo'
  };
});
Enter fullscreen mode Exit fullscreen mode

Para executar o exemplo

git clone https://github.com/rogeriofonseca/angular-spectator-example.git
cd angular-spectator-example
npm install 
npm run test:watch
Enter fullscreen mode Exit fullscreen mode

Resultado Final

Ao executar os testes executando o seguinte comando na raiz do projeto você poderá observar o seguinte resultado.
npm run test:watch

🚧 🚨 Lembrando que apenas para fins de demonstração resolvi deixar um console.log() no código para demonstrar a saída do resultado.

  console.log
    { cep: '01001-000',
      logradouro: 'Praça da Sé',
      complemento: 'lado ímpar',
      bairro: 'Sé',
      localidade: 'São Paulo',
      uf: 'SP',
      ibge: '3550308',
      gia: '1004',
      ddd: '11',
      siafi: '7107',
      enderecoCompleto: 'Praça da Sé, Sé, São Paulo' }

      at src/app/list-cep/list-cep.component.spec.ts:59:13

 PASS  src/app/list-cep/list-cep.component.spec.ts
  ListCepComponent
    ✓ should exist component (93 ms)
    ✓ should exist service (27 ms)
    ✓ should exist apiMocked (27 ms)
    ✓ should check service result (51 ms)
    ✓ should click button (510 ms)

Test Suites: 1 passed, 1 total
Tests:       5 passed, 5 total
Snapshots:   0 total
Time:        4.367 s, estimated 5 s
Ran all test suites related to changed files.

Watch Usage
 › Press a to run all tests.
 › Press f to run only failed tests.
 › Press p to filter by a filename regex pattern.
 › Press t to filter by a test name regex pattern.
 › Press q to quit watch mode.
 › Press Enter to trigger a test run.
Enter fullscreen mode Exit fullscreen mode

Inicializando o Projeto (run start)

Comando para inicializar o projeto

npm run start
Enter fullscreen mode Exit fullscreen mode

O resultado poderá ser visualizado no browser no endereço
http://localhost:4200/
Screen Shot 2021-05-19 at 22.39.11

Link para o repositório do exemplo

GitHub logo rogeriofonseca / angular-spectator-example

An example of Angular with Spectator

Latest comments (4)

Collapse
 
kelsen_brito_ee105604914b profile image
Kelsen Brito

Parabéns Dom!!!
Excelente.

Collapse
 
marcuspaulo profile image
Marcus Paulo

Parabéns Rogério, excelente post. 👏🏻👏🏻👏🏻

Collapse
 
gleiceellen profile image
saudade de liberdade

Parabéns pelo post, Rogério! 👏🏽👏🏽

Collapse
 
wbrunofc profile image
Wallison Bruno Ferreira da Costa

Caramba! super descritivo e direto ao ponto!!!