Angular - Testes de Pipes e Services

wesleyegberto

Wesley Egberto

Posted on May 15, 2021

Angular - Testes de Pipes e Services

No post anterior falamos sobre o básico de testes, neste iremos iniciar os testes unitários das estruturas do Angular.

Outros posts da série:

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) 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:

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)`;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Seguindo o padrão do Angular, criaremos o arquivo de teste strength.pipe.spec.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)');
  });
});
Enter fullscreen mode Exit fullscreen mode

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:

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

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

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

  clear() {
    this.messages = [];
  }
}
Enter fullscreen mode Exit fullscreen mode

Arquivo de teste message.service.spec.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);
  });
});
Enter fullscreen mode Exit fullscreen mode

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:

@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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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

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"));
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

Service com HTTP Client

Para testar uma service que faz uma requisição HTTP podemos utilizar um módulo de teste HttpClientTestingModule 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:

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);
  }
Enter fullscreen mode Exit fullscreen mode

Podemos criar um caso de teste hero.service.spec.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();
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

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

Exemplo de relatório do Karma

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

💖 💪 🙅 🚩
wesleyegberto
Wesley Egberto

Posted on May 15, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related