Em outra oportunidade aqui no blog eu ensinei como escrever testes unitários para smart contracts com o toolkit HardHat, tutorial que você confere neste link. Este conhecimento em questão é o mínimo fundamental quando o assunto é qualidade de smart contracts pois se deixar para testar seu contrato apenas manualmente, estará assumindo um risco grande de deixar fluxos com bugs ou brechas de segurança.
Mas mesmo escrevendo testes unitários, como saber se escrevi uma quantidade de testes que realmente me dê uma segurança que o código está bem testado? Aí que entram as ferramentas de cobertura de código (code coverage) presentes no HardHat e que mostrarei como utilizá-las e interpretá-las hoje.
Caso não saiba, cobertura de código ou cobertura de testes é uma métrica percentual que diz o quanto do seu código-fonte tem testes escritos e passando. Claro, você pode obter essa métrica sob diferentes óticas e isso pode ou não ser relevante para seu projeto, mas partindo do pressuposto que geralmente um código bem testado possui mais qualidade que um não (ou mal) testado, um bom índice de code coverage costuma ser bem visto nas empresas.
Se preferir, você encontra este mesmo conteúdo na metade do vídeo abaixo.
Vamos lá!
#1 – Setup do Projeto
Crie uma pasta no seu computador chamada code-coverage e rode os seguintes comandos para criar um projeto HardHat do zero nela.
1 2 3 4 5 6 7 |
mkdir code-coverage cd code-coverage npm init -y npm i hardhat npx hardhat init |
Avance até o final no assistente de configuração de projeto do HardHat, escolhendo TypeScript e confirmando as demais opções.
Usaremos um smart contract pronto, de um novo token ERC-20 que se você quiser aprender como construir do zero pode fazê-lo com este outro tutorial aqui. Agora se quiser apenas pegar ou olhar o smart contract em questão, ele está no meu GitHub, neste link, chama-se LuizCoin.
Certifique-se de que LuizCoin.sol está na sua pasta contracts (exclua o antigo) e crie um arquivo LuizCoin.test.ts na pasta de testes, excluindo o arquivo original que estava lá. Neste novo arquivo de testes, deixe como abaixo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers"; import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs"; import { expect } from "chai"; import { ethers } from "hardhat"; describe("LuizCoin Tests", function () { async function deployFixture() { const [owner, otherAccount] = await ethers.getSigners(); const LuizCoin = await ethers.getContractFactory("LuizCoin"); const contract = await LuizCoin.deploy(); return { contract, owner, otherAccount }; } it("Should set the right unlockTime", async function () { const { contract, owner, otherAccount } = await loadFixture(deployFixture); //expect(await contract.testFunction()).to.equal(x); }); }); |
Pronto, agora você tem tudo que é necessário para começarmos!
#2 – Code Coverage no HardHat
A primeira coisa que você tem de entender é como obter e como interpretar os indicadores de code coverage do HardHat. Para isso, o primeiro passo é, ao invés de executar o comando regular para executar os testes (npx hardhat test), você deve executar o comando de cobertura de testes abaixo.
1 2 3 |
npx hardhat coverage |
Isso vai fazer com que o HardHat não apenas rode os testes como calcule as métricas de code coverage, lhe apresentando um resultado como abaixo nesta primeira execução.
Saber analisar essa tabela é fundamental para colher os benefícios dessa prática. Da esquerda para direita, temos as colunas: File, Statements (% Stmts), Branches, Functions (% Funcs), Lines e Uncovered Lines. Explicarei cada uma delas a seguir.
File
Aqui temos a pasta que está sendo testada e todos seus arquivos Solidity, um abaixo do outro. Assim, a primeira linha, da pasta, conterá métricas de code coverage da pasta como um todo, considerando todos arquivos. A partir da segunda linha, já teremos as métricas específicas de um único arquivo. Como no meu caso tenho apenas um contrato, estas duas linhas serão sempre iguais.
% Stmts
Statement ou diretiva são comandos de programação. Essa coluna indica o percentual de statements cobertos por testes no seu contrato Solidity. Muitas vezes esta métrica será igual ao da coluna % Lines, mas nem sempre, já que podemos usar mais de uma linha para o mesmo statement ou colocar mais de um em uma mesma linha. Exemplos incluem: imports, chamadas de função, etc
% Branch
Branches ou ramificações são os fluxos ou caminhos possíveis que seu código pode tomar. Essa coluna indica o percentual de fluxos alternativas a sua suíte de testes está cobrindo. Por exemplo, se você tem um if, existirão duas branches: a do if ser verdadeiro e a do if ser falso, e você deveria testar ambos cenários se quiser ter segurança de que todos fluxos estão funcionando.
% Funcs
Nesta coluna temos o percentual de funções cobertas por testes, ao menos um teste por função. Ou seja, você pode ter uma alta cobertura nas funções mas estar péssimo nas branches, por exemplo, apenas escrevendo um teste por função.
% Lines
Nesta coluna temos a análise mais rasa de cobertura de testes que é por linha, mas também a mais fácil de entender. Esse indicador mostra o % de linhas de código do seu contrato que são executadas pelos seus testes. Eu digo que essa análise é mais rasa por que em uma mesma linha você pode ter mais de um fluxo, por exemplo, mas se testar apenas um, a linha vai estar “coberta”.
Uncovered Lines
Esta coluna exibe as linhas ainda não cobertas por testes (vermelho) ou parcialmente cobertas (amarelo). Uma linha parcialmente coberta é aquela que possui ao menos um teste mas que não cobre todas branches da linha. Repare que o espaço é limitado, então apenas algumas linhas são mostradas e conforme você for aumentando a cobertura novas linhas podem surgir.
Além das colunas é interessante entender a escolha de cores da tabela, onde vermelho sempre significa ruim, amarelo razoável e verde bom. Assim, sempre que um indicador estiver abaixo de 50%, ele será considerado ruim, enquanto estiver entre 51% e 79% será considerado razoável e acima disso, bom, que é o que devemos buscar sempre que possível.
Isso é tudo que preciso saber de cobertura de código com HardHat? Claro que não!
#3 – Coverage Report
Imagine o cenário que você foi lá e escreveu todas os testes para cada uma das funções do seu contrato, visando verificar as funcionalidades dele. Pensando nos caminhos principais pelo menos. Você chegará no cenário abaixo dessa forma:
Repare a lista de testes realizados no topo, indicando que passamos por todas as funções e a tabela com as métricas de code coverage abaixo, com quase tudo perfeito (100%), exceto Branches. O que fazer neste caso, como saber qual a branch que está faltando testar?
Algumas vezes você indo até o seu contrato e olhando você acha, mas o Jest entrega pra gente mais que aquela tabela no terminal. Aliás eu diria que a tabela é o output mais básico do code coverage, pois o que realmente nos dá poder para escrever uma boa cobertura de testes é o Coverage Report, que é gerado a cada execução dos testes com cobertura e salvo na pasta coverage, que fica na raiz do seu projeto HardHat.
Dentro dessa pasta você vai achar um index.html, experimente abri-lo no navegador e você terá uma página como abaixo.
Repare que essa tabela apresenta um resumo geral no topo e a listagem de pastas no corpo. Mas mesmo parecendo ser uma versão resumida da tabela do terminal, ela já nos entrega algumas informações adicionais, como o número de branches cobertas e o total de branches existentes no código (3/6), o que já nos ajuda a entender quantos fluxos faltam ser testados.
Mas isso não é o bastante para saber quais branches faltam, certo? Nesse caso, experimente clicar no link da primeira coluna (contracts/) e você será direcionado para o relatório dessa pasta em específico. Esse segundo relatório, no meu caso, é igual ao primeiro já que tenho apenas um contrato na pasta contracts, então já vou clicar novamente no próximo link (LuizCoin.sol) para ver os detalhes da cobertura de código para este arquivo.
Aqui você terá linhas brancas, amarelas ou vermelhas. Ou ainda linhas com algumas “badges”, marcações especiais.
Linhas com fundo branco, sem qualquer marcação, representam que estão cobertas 100% por testes. Enquanto linhas amarelas representam cobertura parcial e linhas vermelhas a ausência de qualquer teste. A essa altura já deve estar acostumado com esta notação, então vou pular para as marcações, como abaixo.
Aqui podemos falar de duas marcações. Primeiro, os multiplicadores (1x) ao lado da numeração das linhas representa quantas vezes essas linhas foram testadas na sua suíte completa. Já a badge “E”, indica que aquela linha possui um condicional e que o ELSE do condicional não foi testado. Neste caso, um require, quer dizer que o único teste que existe para essa linha (1x) só está testando o sucesso, o caminho principal, sem ter um teste para o cenário de erro.
Então seria essa uma das branches não testadas que aparecem no nosso relatório? Exato!
Outra possibilidade de badge seria um “I”, que representa que a condição IF não foi testada (cenário principal).
Assim, seguindo essas pistas do coverage report, podemos criar os testes para os caminhos ainda não testados e alcançar o tão sonhado 100% de cobertura.
Repare na imagem acima, que adicionamos três novos testes para obter 100% de branches cobertas: um de falha na transferência e dois de falha na transferência delegada (transfer from), por razões diferentes (allowance e balance). Você pode dar uma olhada nos testes neste link.
E é isso, basicamente esse é o uso e interpretação da funcionalidade de code coverage disponível no HardHat.
Espero ter ajudado!
Olá, tudo bem?
O que você achou deste conteúdo? Conte nos comentários.