DEV Community

loading...
Cover image for Angular: Teste Unitário com Spectator

Angular: Teste Unitário com Spectator

Rogerio Fonseca
I'm interested in subjects like DevOps, Clean Code, DDD, TDD, algorithms and other things more.
Updated on ・6 min read

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

Discussion (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!!!