Angular - Testes de Pipes e Services
Tags: angular, javascript, beginners, testing
Published: 2021-05-15 04:06:58

No [post](https://dev.to/wesleyegberto/angular-introducao-aos-testes-2m7f) anterior falamos sobre o básico de testes, neste iremos iniciar os testes unitários das estruturas do Angular.

Outros posts da série:

- [Parte 1 - Introdução aos Testes](https://dev.to/wesleyegberto/angular-introducao-aos-testes-2m7f)
- Parte 2 - Testes de Pipes e Services
- [Parte 3 - Testes de Componentes](https://dev.to/wesleyegberto/angular-introducao-aos-testes-2m7f)

Estruturas que iremos testar:

- Pipes
- Services

## Pipes

Como mostrado no post anterior os testes de Pipes não tem segredo.
Podemos suprir as dependências da classe utilizando mocks ou instâncias manuais - recomendo usar mock de modo que possa a testar de fato apenas as regras da pipe.

Sempre que possível, é interessante testar todas as possíveis variações dos valores de entrada ([limites do domínio de entrada](https://pt.wikipedia.org/wiki/An%C3%A1lise_do_valor_limite)) para que não tenhamos nenhum caso não tratato, pois não saberemos quem poderá utilizar nossa Pipe.

Dado a pipe abaixo no arquivo `strength.pipe.ts`:

```ts
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({ name: 'strength' })
export class StrengthPipe implements PipeTransform {
  transform(value: number): string {
    if (value < 0) {
      return '(invalid)';
    } else if (value < 10) {
      return `${value} (weak)`;
    } else if (value >= 10 && value < 20) {
      return `${value} (strong)`;
    } else {
      return `${value} (unbelievable)`;
    }
  }
}
```

Seguindo o padrão do Angular, criaremos o arquivo de teste `strength.pipe.spec.ts`:

```ts
import { StrengthPipe } from './strength.pipe';

describe('Pipe: Strength', () => {
  let pipe: StrengthPipe;

  beforeEach(() => {
    pipe = new StrengthPipe();
  });

  it('should display the strengh with sufix `weak` when between 0 and 10', () => {
    expect(pipe.transform(0)).toBe('0 (weak)');
    expect(pipe.transform(5)).toBe('5 (weak)');
    expect(pipe.transform(9)).toBe('9 (weak)');
  });

  it('should display the strengh with sufix `strong` when between 10 and 20', () => {
    expect(pipe.transform(10)).toBe('10 (strong)');
    expect(pipe.transform(15)).toBe('15 (strong)');
    expect(pipe.transform(19)).toBe('19 (strong)');
  });

  it('should display the strengh with sufix `unbelievable` when >= 20', () => {
    expect(pipe.transform(20)).toBe('20 (unbelievable)');
    expect(pipe.transform(57)).toBe('57 (unbelievable)');
    expect(pipe.transform(100)).toBe('100 (unbelievable)');
  });

  it('should display the invalid when < 0', () => {
    expect(pipe.transform(-1)).toBe('(invalid)');
  });
});
```

## Services

Os testes de services tem uma estrutura similar aos testes de pipes, o que pode complicar os casos de testes são as dependências do service.
Podemos ter uma service que apenas gerencia estado, executa alguma lógica de negócio ou que realiza uma requisição HTTP para uma API.

Caso nossa service utilize alguma lib é recomendado verificar se tal lib tem uma versão de testes que nos ajude a mockar ou simular seu comportamento, permitindo assim uma melhor qualidade no teste.

Para as services normalmente podemos ter:

* testes isolados: casos onde não existe dependência ou casos em que criamos mock de todas elas;
* testes integrados: casos onde testamos o comportamento da nossa service junto com as suas dependências.

### Service Sem Dependências

Abaixo segue um exemplo de uma service que gerencia o estado em um array.

Arquivo `message.service.ts`:

```ts
import { Injectable } from '@angular/core';

@Injectable()
export class MessageService {
  messages: string[] = [];

  add(message: string) {
    this.messages.push(message);
  }

  clear() {
    this.messages = [];
  }
}
```

Arquivo de teste `message.service.spec.ts`:

```ts
import { MessageService } from './message.service';

describe('MessageService', () => {
  // cut (class under testing) - cute para não pegarmos aversão aos testes =)
  let cut: MessageService = null;

  beforeEach(() => {
    cut = new MessageService();
  });

  it('should have no message when starting', () => {
    expect(cut.messages.length).toBe(0);
  });

  it('should add a message and keep it', () => {
    cut.add('message1');

    expect(cut.messages.length).toBe(1);
    const storedMessage = cut.messages[0];
    expect(storedMessage).toBe('message1');
  });

  it('should remove all messages when clear is called', () => {
    cut.add('message to remove');

    cut.clear();

    expect(cut.messages.length).toBe(0);
  });
});
```

### Service com Dependência

Para criar um teste unitário de uma service que tem uma dependência qualquer podemos utilizar o Jasmine para nos ajudar a criar o mock dessa dependência de forma que nos permita mockar seu retorno ou verificar se um método foi chamado.

Como exemplo, tomemos o service abaixo que depende de uma classe `LogService` que tenha um método `log(string): boolean` e que sempre precisa logar o resultado do cálculo:

```ts
@Injectable()
export class CalculationService {
  constructor(private readonly logger: LogService) {}

  calculate(): number {
    const answer = 42; // magic
    const logged = this.logger.log(`Calculation result: ${answer}`);
    // alguma regra que obrigue sempre a logar o resultado
    if (!logged)
      throw new Error("Result was not logged");
    return answer;
  }
}
```

Podemos criar o teste `calculation.service.spec.ts` abaixo:

```ts
describe('CalculationService', () => {
  let mockedLogger: LogService;

  let service: CalculationService;

  beforeEach(() => {
    // criamos o mock da nossa dependência passando os métodos logados
    mockedLogger = jasmine.createSpyObj(['log']);

    // instanciamos nossa service passando a sua dependência
    service = new CalculationService(mockMessageService);
  });

  describe('calculate', () => {
    it('should return 42 and log it', () => {
      // mockamos o retorno do método que dependemos (informa que logou)
      mockMessageService.log.and.returnValue(true);

      const result = service.calculate();

      // validamos o retorno da service
      expect(result).toBe(42);
      // verificamos se o método foi chamado
      expect(mockedLogger.log).toHaveBeenCalledTimes(1);
      // também podemos verificar se foi passado o argumento esperado
      expect(mockMessageService.add).toHaveBeenCalledWith('Calculation result: 42');
    });

    // caso onde testamos o comportamento caso não seja logado
    it('should throw error if result was not logged', () => {
      // mockamos o retorno informando que não logou
      mockMessageService.log.and.returnValue(false);

      // podemos também testar o lançamento de erros
      expect(() => service.calculate()).toThrow(new Error("Result was not logged"));
    });
  });
});
```

### Service com HTTP Client

Para testar uma service que faz uma requisição HTTP podemos utilizar um módulo de teste [HttpClientTestingModule](https://angular.io/guide/http#testing-http-requests) que o Angular disponibiliza.

Para os testes com requisição HTTP precisamos configurar e inicializar o módulo de teste do Angular de modo que ele configure um mock do HttpClient que faça uma requisição falsa ao capturar a URL que foi chamada e retorne um response que precisamos para testar o comportamento da service.

Para configurar o módulo de testes utilizaremos a classe `TestBed` que o Angular fornece para facilitar a configuração do teste.
Essa configuração precisa ser feita antes dos nossos casos de testes, para isso utilizaremos o método `beforeEach` que é invocado antes de cada teste.

Ainda no método `beforeEach`, precisamos recuperar uma instância da classe `HttpTestingController` que o Angular configura para nós, com ela iremos poder validar quais URLs foram chamadas, quantoas vezes e forçar um retorno simulado.

Com a service `hero.service.ts`:

```ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';

import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';

import { Hero } from './hero';

const httpOptions = {
  headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};

@Injectable()
export class HeroService {
  constructor(private http: HttpClient) { }

  // a regra da nossa service é adicionar um simples prefixo no nome
  getHeroes(): Observable<any> {
    return this.http.get('/api/heroes')
      .pipe(tap(heroes => {
        if (heroes && heroes.length) {
          heroes.forEach(hero => {
            hero.name = `Super ${hero.name}`;
          });
        }
      }));
  }

  saveHero(hero: any): Observable<Hero> {
    return this.http.post('/api/heroes', hero, httpOptions);
  }
```

Podemos criar um caso de teste `hero.service.spec.ts`:

```ts
import { TestBed, inject } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';

import { HeroService } from './hero.service';
import { MessageService } from './message.service';

describe('HeroService', () => {
  let httpTestingController: HttpTestingController;

  let heroSvc: HeroService;

  beforeEach(() => {
    // no `providers` precisamos adicionar todas as dependências da nossa service
    TestBed.configureTestingModule({
      imports: [ HttpClientTestingModule ],
      providers: [ HeroService ]
    });

    // recuperamos a instância do controle de teste
    httpTestingController = TestBed.get(HttpTestingController);
    // recuperamos nossa service já configurada com sua dependência (HttpClient)
    heroSvc = TestBed.get(HeroService);
  });

  // aqui garantimos que nossa service e suas dependências conseguem ser criadas
  it('should be able to create a service', inject([HeroService], (heroService: HeroService) => {
    expect(heroService).toBeDefined();
  }));

  // caso nosso método tenha vários testes é interessante agrupar em um `describe` para facilitar a visualização no relatório
  describe('getHeroes', () => {
    // caso onde verificamos se a URL correta foi chamada
    it('should call get with the correct URL', () => {
      // aqui nosso subscriber não fará nada visto que estamos interessando em testar a requisição
      heroSvc.getHeroes().subscribe();

      // podemos verificar a requisição já validando se foi feita para determinada URL
      httpTestingController.expectOne('/api/heroes');

      // verifica as expectativas configuradas no controle de teste
      httpTestingController.verify();
    });

    // case onde verificamos o retorno da service
    it('should update the name with Super prefix', () => {
      heroSvc.getHeroes()
        // aqui podemos utilizar o subscriber para verificar o resultado
        .subscribe(heroes => {
          expect(heroes).toBeTruthy();
          expect(heroes.length).toBe(1);

          const hero = heroes[0];
          expect(hero.name).toBe('Super Odair');
          expect(hero.strength).toBe(42);
        });

      // `expectOne` captura a requisição e retorna um `TestRequest` onde podemos configurar a resposta da requisição
      const req = httpTestingController.expectOne('/api/heroes');
      // aqui setamos o JSON de retorno da URL
      req.flush([ { id: 42, name: 'Odair', strength: 42 } ]);

      // rodamos nossa verificação
      httpTestingController.verify();
    });
  });

  // caso de teste para salvar
  describe('saveHero', () => {
    it('should save a hero', () => {
      heroSvc.saveHero({ name: 'Mr. Anderson', strength: 42 })
        .subscribe(hero => {
          expect(hero).toBeTruthy();
          // verificamos se a service não alterou o resultado da API
          expect(hero.name).toBe('Neo');
          expect(hero.strength).toBe(42);
          // validamos o ID gerado
          expect(hero.id).toBe(1)
        });

      const req = httpTestingController.expectOne('/api/heroes');

      // validamos o JSON da requisição para verificar se service não alterou o que a tela passou
      expect(req.request.body.name).toBe('Mr. Anderson');
      expect(req.request.body.strength).toBe(42);

      // mockamos o retorno criado pela API para rodar o subscriber
      // retornamos outro nome para verificar se a service retornou o response da API =)
      req.flush({ id: 1, name: 'Neo', strength: 42 });
      httpTestingController.verify();
    });
  });
});
```

A classe de teste estruturada acima irá gerar uma seção no relatório do Karma abaixo:

![Exemplo de relatório do Karma](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iz0ojwx8d1umrd1d73wa.png)

No próximo post criaremos alguns testes de componentes de tela.