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 100 km 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.
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).
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é dois parâmetros, uma descrição e uma função, respectivamente;
- Dentro do segundo parâmetro, é onde adicionamos as verificações (expectations).
Exemplo:
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:
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:
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 pelocompileComponents()
, 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. Ofixture.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:
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á.
Podemos observar que a classe deste serviço estende de EntityStore
que contém o método update
, exibido no código abaixo.
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
.
É 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.
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.
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.
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:
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.
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:
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:
Neste ponto já preparamos todas as linhas da spec do ngOnInit()
, resta adicionar as verificações e o comando para executar o hook:
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:
- testar se o retorno da variável spy é true, usando o
toBeTruthy()
; - testar se o método
onClickChatTrigger()
foi chamado, usando a funçãotoHaveBeenCalled()
; - testar se o método
onClickChatTrigger()
foi chamado 1 vez, usando a funçãotoHaveBeenCalledTimes(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 é:
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á:
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
Jest é um poderoso Framework de Testes em JavaScript com um foco na simplicidade.
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.
Victor Hugo Carvalho Alves, Analista de Sistemas na BRQ