Testes com Hardhat
A seguir, é apresentado um exemplo completo de como escrever e executar testes unitários para um contrato inteligente na Ethereum utilizando o Hardhat. Este exemplo incluirá a criação do contrato inteligente, a configuração dos fixtures e a escrita de diversos testes.
Paso 1: Configuração do Projeto
Devemos ter o Hardhat instalado e um projeto básico criado (caso não saibas como fazer isso, consulte o módulo 4).
Certifique-se de ter o plugin
hardhat-toolbox
instalado.npm install --save-dev @nomicfoundation/hardhat-toolbox
Além disso, é necessário adicionar a seguinte linha no início do arquivo
hardhat.config.js
require("@nomicfoundation/hardhat-toolbox");
Paso 2: Definir o contrato a ser testado
Utilizaremos o contrato Token.sol
, que será colocado na pasta contracts
com o seguinte conteúdo:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Token {
string public name = "KIPU";
string public symbol = "KIP";
uint8 public decimals = 18;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
event Transfer(address indexed from, address indexed to, uint256 value);
constructor(uint256 _initialSupply) {
totalSupply = _initialSupply * (10 ** uint256(decimals));
balanceOf[msg.sender] = totalSupply;
}
function transfer(address _to, uint256 _value) public returns (bool success) {
require(_to != address(0), "Invalid address");
require(balanceOf[msg.sender] >= _value, "Insufficient balance");
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
emit Transfer(msg.sender, _to, _value);
return true;
}
}
Paso 3: Escrever os testes
Crie um arquivo de testes chamado TokenTest.js
dentro da pasta test
com o seguinte conteúdo:
const { expect } = require("chai");
const { ethers } = require("hardhat");
const { loadFixture } = require("@nomicfoundation/hardhat-network-helpers");
describe("Token contract", function () {
async function deployTokenFixture() {
const [owner, addr1, addr2] = await ethers.getSigners();
const Token = await ethers.getContractFactory("Token");
const initialSupply = 1000n; // Use BigInt
const token = await Token.deploy(initialSupply);
return { token, owner, addr1, addr2 };
}
it("Should set the right owner", async function () {
const { token, owner } = await loadFixture(deployTokenFixture);
expect(await token.balanceOf(owner.address)).to.equal(1000n * 10n ** 18n); // Use BigInt
});
it("Should assign the total supply of tokens to the owner", async function () {
const { token, owner } = await loadFixture(deployTokenFixture);
const ownerBalance = await token.balanceOf(owner.address);
expect(await token.totalSupply()).to.equal(ownerBalance);
});
describe("Transactions", function () {
it("Should transfer tokens between accounts", async function () {
const { token, owner, addr1, addr2 } = await loadFixture(deployTokenFixture);
await token.transfer(addr1.address, 50n * 10n ** 18n); // Use BigInt
expect(await token.balanceOf(addr1.address)).to.equal(50n * 10n ** 18n); // Use BigInt
await token.connect(addr1).transfer(addr2.address, 50n * 10n ** 18n); // Use BigInt
expect(await token.balanceOf(addr2.address)).to.equal(50n * 10n ** 18n); // Use BigInt
expect(await token.balanceOf(addr1.address)).to.equal(0n); // Use BigInt
});
it("Should fail if sender doesn’t have enough tokens", async function () {
const { token, owner, addr1 } = await loadFixture(deployTokenFixture);
const initialOwnerBalance = await token.balanceOf(owner.address);
await expect(
token.connect(addr1).transfer(owner.address, 1n * 10n ** 18n) // Use BigInt
).to.be.revertedWith("Insufficient balance");
expect(await token.balanceOf(owner.address)).to.equal(initialOwnerBalance);
});
it("Should update balances after transfers", async function () {
const { token, owner, addr1, addr2 } = await loadFixture(deployTokenFixture);
const initialOwnerBalance = await token.balanceOf(owner.address);
await token.transfer(addr1.address, 100n * 10n ** 18n); // Use BigInt
await token.transfer(addr2.address, 50n * 10n ** 18n); // Use BigInt
const finalOwnerBalance = await token.balanceOf(owner.address);
expect(finalOwnerBalance).to.equal(initialOwnerBalance - 150n * 10n ** 18n); // Use BigInt
const addr1Balance = await token.balanceOf(addr1.address);
expect(addr1Balance).to.equal(100n * 10n ** 18n); // Use BigInt
const addr2Balance = await token.balanceOf(addr2.address);
expect(addr2Balance).to.equal(50n * 10n ** 18n); // Use BigInt
});
});
});
Explicação do script acima
Importações e configurações necessárias:
const { expect } = require("chai");
const { ethers } = require("hardhat");
const { loadFixture } = require("@nomicfoundation/hardhat-network-helpers");
chai: Biblioteca de asserções que facilita a verificação dos resultados esperados nos testes.
ethers: Biblioteca para interação com contratos inteligentes e a blockchain.
loadFixture: Função que carrega um estado inicial fixo (fixture) para os testes, garantindo que todos comecem com as mesmas condições.
Depois definimos o fixture
async function deployTokenFixture() {
const [owner, addr1, addr2] = await ethers.getSigners();
const Token = await ethers.getContractFactory("Token");
const initialSupply = 1000n; // Usamos BigInt para evitar desbordamientos
const token = await Token.deploy(initialSupply);
return { token, owner, addr1, addr2 };
}
deployTokenFixture: Esta função implanta o contrato
Token
com um fornecimento inicial. Também obtém três contas para serem utilizadas nos testes (owner, addr1, addr2).
Primeiro teste: Verificar o proprietário
it("Should set the right owner", async function () {
const { token, owner } = await loadFixture(deployTokenFixture);
expect(await token.balanceOf(owner.address)).to.equal(1000n * 10n ** 18n);
});
Propósito: Verificar se o proprietário inicial (a conta que implanta o contrato) recebe o fornecimento total de tokens.
Proceso:
Implantar o contrato utilizando o fixture.
Obter o saldo do proprietário.
Verificar se o saldo é igual ao fornecimento inicial (1000 tokens convertidos para a unidade mínima).
Segundo teste: Verificar o fornecimento total
it("Should assign the total supply of tokens to the owner", async function () {
const { token, owner } = await loadFixture(deployTokenFixture);
const ownerBalance = await token.balanceOf(owner.address);
expect(await token.totalSupply()).to.equal(ownerBalance);
});
Propósito: Garantir que o fornecimento total de tokens seja atribuído corretamente ao proprietário.
Processo:
Implantar o contrato utilizando o fixture.
Obter o saldo do proprietário.
Verificar se o saldo do proprietário é igual ao fornecimento total de tokens
Terceiro teste: Transferência de tokens
describe("Transactions", function () {
it("Should transfer tokens between accounts", async function () {
const { token, owner, addr1, addr2 } = await loadFixture(deployTokenFixture);
await token.transfer(addr1.address, 50n * 10n ** 18n);
expect(await token.balanceOf(addr1.address)).to.equal(50n * 10n ** 18n);
await token.connect(addr1).transfer(addr2.address, 50n * 10n ** 18n);
expect(await token.balanceOf(addr2.address)).to.equal(50n * 10n ** 18n);
expect(await token.balanceOf(addr1.address)).to.equal(0n);
});
Propósito: Verificar se os tokens são transferidos corretamente entre contas.
Processo:
Implantar o contrato utilizando o fixture.
Transferir 50 tokens do
owner
paraaddr1
.Verificar que
addr1
enha recebido 50 tokens.Transferir 50 tokens de
addr1
paraaddr2
.Verificar que
addr2
tenha recebido 50 tokens e queaddr1
tenha um saldo de 0 tokens.
Quarto teste: Verificar que uma conta não pode transferir se não tiver saldo suficiente
it("Should fail if sender doesn’t have enough tokens", async function () {
const { token, owner, addr1 } = await loadFixture(deployTokenFixture);
const initialOwnerBalance = await token.balanceOf(owner.address);
await expect(
token.connect(addr1).transfer(owner.address, 1n * 10n ** 18n)
).to.be.revertedWith("Insufficient balance");
expect(await token.balanceOf(owner.address)).to.equal(initialOwnerBalance);
});
Propósito: Garantir que a transferência falhe se o remetente não tiver tokens suficientes.
Proceso:
Implantar o contrato utilizando fixture.
Tentar transferir 1 token de
addr1
(que não possui tokens) para oowner
.Verificar que a transação seja revertida com a mensagem "Insufficient balance".
Verificar que o saldo do
owner
não tenha sido alterado.
Quinto teste: Atualização dos saldos após uma transferência
it("Should update balances after transfers", async function () {
const { token, owner, addr1, addr2 } = await loadFixture(deployTokenFixture);
const initialOwnerBalance = await token.balanceOf(owner.address);
await token.transfer(addr1.address, 100n * 10n ** 18n);
await token.transfer(addr2.address, 50n * 10n ** 18n);
const finalOwnerBalance = await token.balanceOf(owner.address);
expect(finalOwnerBalance).to.equal(initialOwnerBalance - 150n * 10n ** 18n);
const addr1Balance = await token.balanceOf(addr1.address);
expect(addr1Balance).to.equal(100n * 10n ** 18n);
const addr2Balance = await token.balanceOf(addr2.address);
expect(addr2Balance).to.equal(50n * 10n ** 18n);
});
Propósito: Verificar se os saldos são atualizados corretamente após as transferências.
Proceso:
Implantar o contrato utilizando o fixture.
Transferir 100 tokens do
owner
paraaddr1
.Transferir 50 tokens do
owner
paraaddr2
.Verificar se o saldo final do
owner
é igual ao saldo inicial menos 150 tokens.Verificar se
addr1
possui 100 tokens eaddr2
possui 50 tokens.
Passo 4: Executar os testes
Para executar os testes, utilize o seguinte comando no terminal:
npx hardhat test
Este comando executará todos os testes definidos no arquivo TokenTest.js
e exibirá os resultados no console.
Se os testes forem bem-sucedidos, você verá um resultado semelhante a este:

Parabéns!! Você executou com sucesso seus primeiros testes. Agora é hora de começar a testar outros contratos que você tenha criado.
O Hardhat também permite verificar a cobertura dos seus testes. Para isso, basta executar o seguinte comando:
npx hardhat coverage
E você obterá um relatório semelhante a este

Mais uma coisa: Chai
Neste exemplo, utilizamos Chai para realizar nossos testes. Chai é uma biblioteca de assertions (afirmações) para Node.js que pode ser combinada com qualquer framework de testes, como o Hardhat. No contexto de testes para contratos inteligentes, o Chai é amplamente utilizado devido à sua sintaxe amigável e capacidades robustas de asserção.
Para utilizar o Chai, primeiro é necessário importar a biblioteca e, em seguida, usar seus métodos nas suas asserções dentro dos testes. Aqui está um exemplo básico de configuração:
const { expect } = require("chai");
Chai oferece três estilos principais de asserção:
Assert: Estilo clássico baseado em funções.
Expect: Estilo BDD (Behavior-Driven Development), mais legível.
Should: Outro estilo BDD, que adiciona propriedades ao
Object.prototype
.
No nosso exemplo, utilizamos o estilo expect
, pois é um dos mais usados em testes de contratos inteligentes com Hardhat, graças à sua clareza e sintaxe intuitiva.
Principais comandos do Chai
Igualdade (
equal
,eql
)
equal
verifica igualdade estrita (===
).eql
verifica igualdade profunda (ideal para objetos e arrays).
expect(1).to.equal(1);
expect({ foo: 'bar' }).to.eql({ foo: 'bar' });
Booleanos (
true
,false
)
Verifica se um valor é verdadeiro ou falso.
expect(true).to.be.true;
expect(false).to.be.false;
Existência (
exist
)
Verifica se um valor não é null
nem undefined
.
let foo = 'bar';
expect(foo).to.exist;
Tipos (
a
,an
)
Verifica o tipo de um valor.
expect('foo').to.be.a('string');
expect({ foo: 'bar' }).to.be.an('object');
Conteúdo (
include
,contain
)
Verifica se um valor contém outro (em arrays, strings ou objetos).
expect([1, 2, 3]).to.include(2);
expect('foobar').to.contain('foo');
expect({ foo: 'bar', baz: 'qux' }).to.include({ foo: 'bar' });
Comprimento (
lengthOf
)
Verifica o comprimento de um array, string ou Map.
expect([1, 2, 3]).to.have.lengthOf(3);
expect('foo').to.have.lengthOf(3);
Maior e menor que (
above
,below
)
Verifica se um valor é maior ou menor que outro.
expect(10).to.be.above(5);
expect(5).to.be.below(10);
Aproximação (
closeTo
)
Verifica se um número está próximo de outro, dentro de uma margem de erro.
expect(1.5).to.be.closeTo(1.4, 0.1);
Emissão de eventos (
emit
,withArgs
)
No contexto de testes de contratos inteligentes, verificamos se um evento é emitido com os argumentos corretos.
await expect(contract.emitEvent()).to.emit(contract, "EventName").withArgs(expectedArgs);
Para mais informações sobre Chai, consulte a documentação do Hardhat.
Last updated