Angular: Teste Unitário com Spectator
Tags: angular, spectator, typescript, braziliandevs
Published: 2021-05-18 00:03:10

# Introdução ao Tema <a name="introducao-ao-tema"></a>
Neste post vou te mostrar um exemplo de como podemos utilizar o [Spectator](https://ngneat.github.io/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](https://viacep.com.br/) como exemplo para ser consumido.

# Vá direto ao ponto
* [Apresentação do Problema](#apresentacao-do-problema)
* [Configuração](#configuracao)
* [Exemplo de retorno da Request](#exemplo-de-retorno-da-request)
* [Modelo de Resposta da Requisição](#modelo-de-resposta-da-requisicao)
* [Construindo Cenários e Declarando Dependências](#declarando-dependencias)
* [Definição de Mock ou de retorno Fake](#definicao-de-mock)
* [Construção da Resposta Fake](#construcao-da-resposta-fake)
* [Validação de regras de negócios](#regras-de-negocio)
* [Verificações](#verificacoes)
* [Exemplo Completo](#exemplo-completo)
* [Para executar o exemplo](#executar-exemplo)
* [Resultado Final](#resultado-final)
* [Inicializando o Projeto](#run-start)
* [Link para o repositório do exemplo](#link-github)

## Apresentação do Problema <a name="apresentacao-do-problema"></a>
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](https://viacep.com.br/)

 
## Configuração <a name="configuracao"></a>
Para adicionar a dependência do spectator
```shell
npm install @ngneat/spectator --save-dev
```

## Exemplo de retorno da Request <a name="exemplo-de-retorno-da-request"></a>
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.
```shell
curl -X GET http://viacep.com.br/ws/38660000/json/
```
## Modelo de resposta da requisição<a name="modelo-de-resposta-da-requisicao"></a>
O resultado da execução será um modelo JSON como este:
```json
{
  "cep": "38660-000",
  "logradouro": "",
  "complemento": "",
  "bairro": "",
  "localidade": "Buritis",
  "uf": "MG",
  "ibge": "3109303",
  "gia": "",
  "ddd": "38",
  "siafi": "4185"
}
```

## Construindo Cenários e Declarando Dependências<a name="declarando-dependencias"></a>
### 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.**

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

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

## Definição de **Mock** ou de retorno Fake <a name="definicao-de-mock"></a>
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".

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

## Construção da resposta Fake <a name="construcao-da-resposta-fake"></a>
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)
```

```Javascript
  // Fake Object
  const fakeResponse: Address = {
    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'
  };

  // Dados Esperados
  const expectData: Address = {
    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'
  };
```

## Validação de regras de negócios<a name="regras-de-negocio"></a>
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 <a name="verificacoes"></a>
Após a construção do cenário nosso foco deverá ser na construção das nossas verificações ou asserções.

:construction: :rotating_light: 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.

```Javascript
  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);
  });
```

# Exemplo completo:<a name="exemplo-completo"></a>
:page_facing_up: [Link para arquivo no Github](https://github.com/rogeriofonseca/angular-spectator-example/blob/main/src/app/list-cep/list-cep.component.spec.ts)

```Javascript
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: 'Sé',
    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: 'Sé',
    localidade: 'São Paulo',
    uf: 'SP',
    ibge: '3550308',
    gia: '1004',
    ddd: '11',
    siafi: '7107',
    enderecoCompleto: 'Praça da Sé, Sé, São Paulo'
  };
});
```

## Para executar o exemplo <a name="executar-exemplo"></a>
```shell
git clone https://github.com/rogeriofonseca/angular-spectator-example.git
cd angular-spectator-example
npm install 
npm run test:watch
```

## Resultado Final <a name="resultado-final">
Ao executar os testes executando o seguinte comando na raiz do projeto você poderá observar o seguinte resultado.
**npm run test:watch**

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

```shell
  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.
```

# Inicializando o Projeto (run start)<a name="run-start">
Comando para inicializar o projeto
```shell
npm run start
```

O resultado poderá ser visualizado no browser no endereço 
`http://localhost:4200/`
![Screen Shot 2021-05-19 at 22.39.11](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lkjf075gf1og04ywjsjt.png)
 

# Link para o repositório do exemplo <a name="link-github"></a>
{% github rogeriofonseca/angular-spectator-example/ no-readme %}