Testes unitários com Jest
Tags: jest, angular, testing
Published: 2022-07-19 20:20:47

> Jest é um *framework* de teste unitário de código aberto em *JavaScript* criado pelo *Facebook*, baseado *framework Jasmine*. Seu diferencial para o concorrente *Jasmine* seria a popularidade, flexibilidade e velocidade de execução.

**Contexto:** O presente artigo é focalizado em diferentes exemplos de raciocínios, *expectations* e *matchers* para testes unitários com Jest em um ambiente que utiliza o *framework* SPA Angular.

**Motivação** Existem poucos materiais que explicam linha por linha a montagem da suíte e escrita de testes complexos.

**Escopo:** O presente artigo é recomendado a usuários que já tenham base conceitual sobre o tema de testes unitários em componentes. Os exemplos aqui citados são complexos, não estão disponíveis em um repositório e também não focaliza na instalação da ferramenta, então este material é considerado complementar ao entendimento introdutório do *framework* *Jest*. Dito isso, ainda sim foi construída uma estrutura lógica que parte dos conceitos iniciais, detalha a montagem da suíte de testes no componente e finaliza na escrita/execução da *spec* com foco na métrica do aumento de cobertura de testes no *SonarQube*.

**Objetivo:** aqui nós iremos de 0 a 100km muito rápido. Mostrando como planejar e escrever as *specs* para que ao final, você seja capaz de agir por conta própria.

### Instalação

Recomendo instalar além do *Jest*, o *Jest-CLI* também para montar um script de execução de testes mais detalhado e que atenda a suas necessidades, abaixo segue o *link* para instalação:

https://jestjs.io/pt-BR/docs/getting-started

Nos próximos tópicos serão explicados alguns conceitos importantes para configuração e escrita dos testes unitários.

### Suíte de testes

Servem para definir o escopo do que está sendo testado.
* Dentro de uma aplicação existem várias suítes de testes;
* Alguns exemplos de suítes seriam: Cálculos matemáticos, Cadastro de Clientes, consulta de cadastrados,...
* No *Jest*, a suíte é uma função global *Javascript* chamada `describe`, que possui dois parâmetros, que seriam sua descrição e os testes (*specs*).

Exemplo:

```typescript
describe("Operação de Adição", () => { });
```

### Testes (*specs*)

* *Specs* são os testes que validam uma suíte de testes;
* Assim como as suítes, ela é uma função global Javascript chamada ‘*it*’, que contém dois parâmetros, uma descrição e uma função, respectivamente;
* Dentro do segundo parâmetro, é onde adicionamos as verificações (*expectations*).

Exemplo:

```typescript
it("deve garantir que 1 + 9 = 10", () => { });
```

### Verificações (*Expectations*)

* Verificações servem para validar um resultado de um teste;
* O Jest possui uma função global Javascript chamada ‘expect’, que recebe um parâmetro como argumento, que é o resultado a ser verificado;
* O ‘expect’ deve ser utilizado em conjunto com uma comparação (*Matcher*), que conterá o valor a ser comparado;
* Uma *Spec* poderá conter uma ou mais verificações;
* Uma boa prática é sempre manter as verificações no final da função.

Exemplo:

```typescript
expect(Calculadora.adicionar(1, 9)).toBe(10);
```

### Configuração da suíte de testes

Ao escrever testes você tem algum trabalho de configuração que precisa acontecer antes de executa-los. Caso exista algo que precisa ser executado repetidamente antes ou depois para muitos testes, você pode usar os `hooks`. Para o exemplo dado utilizaremos a função fornecida pelo Jest: `beforeEach`, que basicamente repetirá tudo que é envolto por ele antes de cada teste realizado.

> Assim que criamos um novo componente com auxílio do CLI do angular, automaticamente é gerado um arquivo *spec* com uma configuração base. Conforme código abaixo:

```typescript
import { ComponentFixture, TestBed } from  '@angular/core/testing';
import { NovoComponent } from  './novo.component';
import { NovoModule } from  './novo.module';

describe('NovoComponent', () => {
	let  component: NovoComponent;
	let  fixture: ComponentFixture<NovoComponent>;

	beforeEach(() => {
		TestBed.configureTestingModule({
			imports: [ NovoModule ],
			declarations: [],
			providers: []
		})
		.compileComponents();
		fixture = TestBed.createComponent(NovoComponent);
		component = fixture.componentInstance;
		fixture.detectChanges();
	});

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

Analisando o código acima. Percebe-se o uso do `describe` para criar a suíte de testes para o `NovoComponent`, podemos ver que existem duas variáveis declaradas `component` e `fixture`, na primeira a "tipagem" é o nome da classe que foi criada, na segunda usa o `componentFixture` para ter acesso ao DOM, depurar e testar o componente. No próximo comando, encontra-se a função `beforeEach`, já descrita anteriormente. Por convenção do Angular, adotamos que cada componente obrigatoriamente deve estar contido em um módulo, portanto dentro da estrutura `beforeEach` sempre importaremos o modulo que está declarado o componente a ser testado. Deve-se adicionar ao `providers` as dependências que estão sendo injetadas no arquivo *typescript*.

> Se necessário também, será adicionado aos _providers_, classes que estendem as dependências injetadas.

Após a compilação destes componentes pelo`compileComponents()`, utilizamos o `TestBed`, que cria um módulo Angular de teste que podemos usar para instanciar componentes, executar injeção de dependência afim de configurar e inicializar o ambiente para teste. Na próxima linha de código o `componentInstance` é usado para acessar a instância da classe do componente raiz e o `fixture`é um *wrapper* para um componente e seu *template*. O`fixture.detectChanges()` será acionado para qualquer mudança que aconteça no DOM. 
Por fim, serão adicionados os testes de unidade utilizando a estrutura "*it*". No código acima podemos ver um exemplo padrão de teste unitário que verifica se o componente está sendo criado. É de suma importância que neste ponto aconteça a primeira verificação de execução do teste unitário, pois o mesmo nos dirá se a suíte de testes foi corretamente montada. 

### Mockando Serviços

> A partir de agora recomendo fortemente que utilize o recurso de divisão de tela em seu editor de código, desta forma permitirá a visualização de um lado, o arquivo *typescript* e do outro, o arquivo *spec*. 

O *mock* das dependências injetadas vai nos permitir testar nosso componente de maneira isolada, sem nos preocuparmos com as demais dependências da aplicação. Em tese será criada uma instância de objeto com dados "*fake*", que refletirá toda vez que a dependência for requisitada.

Primeiro ponto a ser observado no código são as variáveis que precisam ser inicializadas e as dependências a serem injetadas:

```typescript
import { Component, OnInit, ViewChild, ElementRef, OnDestroy } from  '@angular/core';
import { Subscription } from  'rxjs';

import { ChatOptionsQuery } from  'src/chat/store/chat-options/chat.options.query';

@Component({
	selector:  'app-chat-trigger',
	templateUrl:  './chat-trigger.component.html',
	styleUrls: ['./chat-trigger.component.scss'],
})
export class ChatTriggerComponent implements OnInit, OnDestroy  {
	totalPendingMessages = 0;
	maxMessagesCounter = 100
	chatTitle: string;
	chatMessage: string;
	openTooltip: boolean;
	ariaLabel:string;
	chatTitleSub$: Subscription;
	chatMessageSub$: Subscription;

	constructor(
		private chatOptionsQuery: ChatOptionsQuery,
		private appViewStore: AppViewStore,
	) { }

	onHide(): void {
		this.appViewStore.update((state: AppViewState) => ({
			...state,
			chatOpen: false,
			chatMinized: true,
			floatChat: true,
		}));
	}
```

O serviço `AppViewStore` é utilizado para chamar o método `update` neste componente. Neste ponto é muito importante ter cuidado, pois como podemos ver no código abaixo, ao acessar este serviço o método `update` não se encontra lá.

```typescript
@Injectable({
providedIn: 'root'
})
@StoreConfig({ name: 'AppView' })
export class AppViewStore extends EntityStore<AppViewState> {
	constructor() {
		super(initialStateAppView);
	}
}
```

Podemos observar que a classe deste serviço estende de `EntityStore` que contém o método `update`, exibido no código abaixo.

```typescript
export declare class EntityStore extends Store<S> {
	…
	update(newState: UpdateStateCallback<S>): any;
```

Entendendo esse cenário, deve-se criar um *mock* dessas duas classes e adicionar o método `update` na classe *mockada* com o valor `MockEntityStore`.

> Seguindo convenção do Angular, o nome dos *mocks* são formados pelo nome `Mock + nome do serviço`.

```typescript
const MockAppViewStore = { };
const MockEntityStore = {
	update() {
		return true
	}
};

beforeEach(() => {
	TestBed.configureTestingModule({
		imports: [ ChatTriggerModule],
		declarations: [],
		providers: [
			{ provide: AppViewStore, useValue: MockAppViewStore },
			{ provide: EntityStore, useValue: MockEntityStore },
		]
})
.compileComponents();
fixture = TestBed.createComponent(ChatTriggerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
```

> É imprescindível que o nome do método ou da variável *mockada* sejam idênticos. Porém como exibido no exemplo acima, o parâmetro passado no método `update` não foi *mockado*, além disso, foi inventado um valor booleano de retorno. E isso é o suficiente para executar os testes.

### Criando testes unitários na prática

Jest utiliza de "*matchers*" (combinadores) para realizar os testes efetivamente. Existem diversos *matchers* para cada situação em particular dentro do contexto de testes. Os *matchers* são implementados a partir da chamada de `expect()`. Para inserir um exemplo com uma complexidade maior, antes de tudo é necessário entendermos o conceito e como implementar as funções de *mock*.

#### Funções mock

* Permitem criar funções e módulos falsos que simulam uma dependência. 
* Com o *mock* é possível interceptar chamadas dessa função (e seus parâmetros) pelo código sendo testado. 
* Permite interceptar instâncias de funções construtoras quando implementadas usando *new*. 
* Permitem a configuração dos valores retornados para o código sob teste.

É comum encontrar em outros artigos a utilização do comando `jest.fn()` para criar funções *mock*, porém o presente arquivo utiliza uma sintaxe semelhante a do Jasmine, por tanto serão criadas as funções *mock* usando o comando `Jest.spyOn(objeto, nomeDoMétodo`) encadeado por exemplo com a função _`mockImplementation`_ que possibilita a substituição da função original.

Abaixo teremos alguns exemplos de *matchers* juntamente com as funções *mock*.

##### Exemplo

Usaremos este código em *typescript* como base para este primeiro exemplo, com o intuito de testar o ciclo de vida (*lifecycle hook*) `ngOnInit()` do Angular.
```typescript
@Input('controls') controls: controls;
@Input("session") session: Session;

public floatChat$: Observable<boolean>;
public chatOpen$: Observable<boolean>;

public  joined: boolean;
public  joined$: Subscription;

constructor(
	public  appViewQuery: AppViewQuery,
) {
}

ngOnInit(): void {
	this.session = typeof  this.session == "string" ? JSON.parse(this.session) : this.session;
	this.controls = typeof  this.controls == "string" ? JSON.parse(this.controls) : this.controls;

	this.floatChat$ = this.appViewQuery.floatChat$;
	this.chatOpen$ = this.appViewQuery.chatOpen$;

	this.joined$ = this.appViewQuery.joined$.subscribe((data:boolean)=>{
		this.joined = data;
	});

	if (this.controls?.alwaysOpenChat) {
		this.onClickChatTrigger();
	}
}
```

Hora de colocar o que foi explicado desde o inicio do artigo, essa analise inicial é de extrema importância para definirmos o plano de ação para criar os testes sobre o `ngOnInit()`. Nas duas primeiras linhas desse *hook* temos dois *if's ternários*, que utilizam as variáveis `session` e `controls`  que tem suas próprias interfaces. Primeiro passo é acessar tais interfaces e criar um *mock* nos moldes dela. 

```typescript
export interface Session {
  "contactId"?: string,
  "sessionId": string,
  "rede": string,
  "channel": channel,
  "nickname": string
}

export enum channel{
  "INTERNET_ON" = "INTERNET_ON",
  "INTERNET_OFF" = "INTERNET_OFF",
  "MOBILE_OFF" = "MOBILE_OFF", 
  "MOBILE_ON" = "MOBILE_ON"
}

export  interface  controls {
	alwaysOpenChat: boolean,
	buttonClose: boolean,
	nicknameChat?: string,
	nicknameAgent?: string,
	iconChat?: string,
}
```
Adicionaremos tais *mocks* de forma global (acesso em qualquer estrutura dentro deste arquivo *spec*). Caso em próximos testes seja preciso modificar algum valor, basta fazer isso dentro da estrutura `it`. 
Serão adicionados dois *mocks* para a variável `session`, a primeira em formato de *string* e a segunda como *Object*. Desta forma poderá ser testado o `JSON.parse` dentro do "*if*" ternário.

```typescript
describe('ChatComponent', () => {
	let component: ChatComponent;
	let fixture: ComponentFixture<ChatComponent>;
	
	const  mockSessionString: any = '{"contactId": "", "sessionId": "", "rede": "", "channel": "INTERNET_ON", "nickname": ""}';
	
	const mockSessionObject: Session = {
		contactId: '',
		sessionId: '',
		rede: '',
		channel: 'INTERNET_ON' as channel,
		nickname: ''
	};

	const mockControls: controls = {
	    alwaysOpenChat: true,
	    buttonClose: true,
	    nicknameChat: '',
	    nicknameAgent: '',
	    iconChat: '',
	}
...
}
```

Agora vamos iniciar a edição da *spec* para este *hook*. Lembrando que como configurado anteriormente criamos uma variável `component` que refere-se a uma instância da classe a ser testada, então iremos atribuir os *mocks* criados a instância da classe para este teste em específico:

```typescript
fit('Should test ngOnInit', () => {
    component.session = mockSessionString;
    component.controls = mockControls;
    ...
}
```
> Adicionando a letra "f" a estrutura "*it*", na hora que executar os testes, essa será a única *spec* a ser testada dentro desta suíte. 

Continuando a analise do *hook*, nas próximas três linhas atribuímos a duas variáveis *observables* do tipo *boolean* e a uma do tipo "*subscription()" valores da dependência `AppViewQuery`. Neste ponto precisamos adicionar tal dependência no *providers* da suíte de testes e além disso adicionar as variáveis *mockadas*.

```typescript
@Injectable({ providedIn:  'root' })
export  class  AppViewQuery  extends  QueryEntity<AppViewState> {
	floatChat$ =this.select("floatChat");
	chatOpen$ =this.select("chatOpen");
	joined$ =this.select("joined");
```

Quando passamos o *mouse* por cima do método, o mesmo nos mostra a "tipagem" do que é retornado, e para método `select()` é um `Observable<boolean>`, com essa informação criaremos o *mock*, iremos utilizar o função `of()` do *RxJS*:
 
```typescript
const MockAppViewQuery = {
    floatChat$: of(false),
    chatOpen$: of(true),
    joined$: of(false)
};

beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [],
      declarations: [ChatComponent],
      providers: [
        { provide: AppViewQuery, useValue: MockAppViewQuery }
      ]
    })
```
Analisando o restante do  *hook*, temos uma condição e que pra o cenário que montamos irá retornar verdadeiro pois `this.controls?.alwaysOpenChat` existe. Desta forma teremos que *mockar* o método que encontra-se dentro da condicional `if()`,  para este exemplo userei o *mockImplementation()*, reescrevendo (de forma aleatória) o retorno do método para um *boolean true*:

```typescript
fit('Should test ngOnInit', () => {
    component.session = mockSessionString;
    component.controls = mockControls;

    const spyOnClickChatTrigger = jest.spyOn(component, 'onClickChatTrigger').mockImplementation(()=> {
      return true;
    });
    ...
}
```

Neste ponto já preparamos todas as linhas da *spec* do `ngOnInit()`, resta adicionar as verificações e o comando para executar o *hook*:

```typescript
fit('Should test ngOnInit', () => {
	//PREPARAÇÃO
    component.session = mockSessionString;
    component.controls = mockControls;

    const spyOnClickChatTrigger = jest.spyOn(component, 'onClickChatTrigger').mockImplementation(()=> {
      return true;
    });
    
    //EXECUÇÃO
	component.ngOnInit(); //LINHA PARA EXECUTAR O HOOK
	
	//VERIFICAÇÃO
	expect(component.session).toEqual(mockSessionObject);
    expect(component.controls).toBe(mockControls);

    component.floatChat$.subscribe((res: boolean)=>{
      expect(res).toBeFalsy();
    });

    component.floatChat$.subscribe((res: boolean)=>{
      expect(res).toBeTruthy();
    });

    component.chatOpen$.subscribe(()=>{
      expect(component.joined).toBeFalsy();
      done();
    })

    expect(spyOnClickChatTrigger).toHaveBeenCalled();
 });
```

Podemos dizer que a montagem dos testes unitários sempre seguem uma estrutura simples dividida em 3 partes, definida como comentário no código acima. Na **preparação** iremos organizar tudo necessário para realização deste teste; Na **execução** vamos de fato rodar os testes; Por fim na **verificação** definiremos qual o resultado que esperamos.

> Os matchers **toBe** e **toEqual** são usados para testar equidade numérica, sendo que o primeiro compara em adicional a tipagem e o segundo somente os valores.

**1ª verificação:** o cenário foi preparado para que a variável `session` passe pelo `JSON.parse()` do "*if*" ternário. Desta forma quando comparado com o *mock* em formato de objeto deverá retornar os mesmo valores.

**2ª verificação:** o cenário foi preparado para que a variável `controls` entrasse na condição falsa do "*if*" ternário e retornasse o mesmo objeto com a mesma tipagem. 

> A função **toBeTruthy** e **toBeFalsy** testam se o resultado passado tem valor que pode ser passado como **true** e **false**, respectivamente, em um if.

**3ª, 4ª e 5ª verificações:** para estes casos precisamos nos inscrever nos *observables* para testar se o retorno *mockado* da depedência `AppViewQuery` é condizente com o recebido pelas variáveis `floatChat$`, `chatOpen$` e `joined`. Para tipo de verificações com assíncrono, usamos um artificio de passar 1 argumento na função "*it*" chamado de `done`. Assim que o houver a última verificação de assíncrono nós chamamos a função `done();`, que permitirá que de fato a comparação dos ***expects*** sejam realizadas.

**6ª verificação:** o *mock* da variável `controls` foi preenchido para que entrasse na estrutura `if()`. Dito isso, neste caso criamos um *spy* que retornará ***true*** toda vez que o método for chamado. Para este caso podemos realizar diferentes testes: 
1. testar se o retorno da variável *spy* é ***true***, usando o `toBeTruthy()`; 
2. testar se o método `onClickChatTrigger()` foi chamado, usando a função `toHaveBeenCalled()`; 
3. testar se o método `onClickChatTrigger()` foi chamado 1 vez, usando a função `toHaveBeenCalledTimes(1)`. Escolhemos usar a opção 2. 

Agora devemos executar a suíte de testes e verificar se os testes obtiveram sucesso.

### Execução

O comando base para executar a suíte de testes é:

```typescript
npm run test
```

Porém quando o CLI do Jest está instalado no projeto, o mesmo nos oferece suporte a argumentos *camelCase* e *tracejados*, então podemos combinar 1 ou mais scripts ao código acima. Exemplo:

* `--detectOpenHandles`
	Tenta coletar e imprimir os manipuladores que estejam abertos impedindo o Jest de sair de forma limpa.

* `--silent`
Evita que testes imprimam mensagens no console.

* `--coverage`
Indica que as informações de coleta do teste devem ser coletadas e reportadas no console.

* `--ci`
Jest assume a execução em um ambiente de CI (integração contínua). Alterando o comportamento quando é encontrado um novo "snapshot". Em vez do comportamento normal de armazenar um novo "snapshot" automaticamente, o teste irá falhar e exigir Jest ser executado com `--updateSnapshot`.

Para executar os testes unicamente do arquivo citado acima, utilizamos a seguinte sintaxe:

`npm test -- Chat.component.spec.ts`

o resultado será:

```text
 PASS  src/chat/Chat.component.spec.ts (119.938 s)
  ChatComponent
    √ Should test ngoninit (155 ms)
    ○ skipped Should test create component
    ○ skipped Should test ngOnChanges
    ○ skipped Should test ngAfterViewInit
    ○ skipped Should test load
    ○ skipped Should test hasAttribute
```

Percebemos que nossos testes passaram com êxito!! Ele ignora os testes nos demais métodos pois especificamos com "*fit*" a *spec* do `ngOnInit()`.

### Referências

https://jestjs.io/pt-BR/

https://cursos.alura.com.br/forum/topico-jasmine-x-jest-179443

https://www.devmedia.com.br/teste-unitario-com-jest/41234#:~:text=Jest%20%C3%A9%20um%20framework%20de,dentro%20da%20comunidade%20de%20JavaScript.


### Revisão e agradecimento
Agradeço a João Paulo Castro Lima pela ideia e apoio na confecção deste artigo e também aos meus amigos revisores:

Elves Gomes Neves Santos;
Francis Gomes Santos;
Matheus Vinicius Geronimo Fald;
Flávio Takeuchi.