🇧🇷
Ethereum Developer Pack - PT
  • Ethereum Developer Pack - PT
  • Módulo 1
    • Introdução a Smart Contracts
      • Fundamentos de Blockchain
        • Histórico
        • Bitcoin
        • O que é Blockchain
        • Conceitos-chave em Blockchain
        • Como funciona a Blockchain
        • Tipos de Blockchain
        • Modelos de Consenso
      • A nova Internet
        • Web 3
        • Elementos Fundamentais
        • Impacto do Ethereum em Diversos Setores
      • Wallets
        • Componentes de uma wallet
        • Tipos de Wallet
        • Códigos Mnemônicos
      • Ethereum 101
        • Smart Contracts
        • Contas
          • Tipos de contas
          • Conteúdo das contas
        • Transações
          • Componentes
          • Ciclo de vida
        • Gás
        • Solidity
        • EVM
          • A máquina de estados
          • Opcodes
          • Como funciona a EVM
          • Clientes de execução
          • DApps
      • Blockchain Explorer
        • Funções de um blockchain explorer
        • Beneficios de utilizar um blockchain explorer
      • Remix
        • Características do Remix
        • Workspaces ou espaços de trabalho
        • Carregar e compilar um contrato
        • Implantar na máquina virtual do Remix (Remix VM)
        • Interagindo com funções
        • Deployar em uma rede pública
      • Crie seu primeiro Smart Contract
  • Módulo 2
    • Fundamentos de Solidity
      • Hello World
      • Tipos de Dados
      • Funções
      • Variáveis
        • Exercício 1
      • Operadores
        • Ejercicio 2
      • Constructor
        • Exercício 3
      • Convenções de nomenclatura
      • Tipos de armazenamento para variáveis
      • Estruturas de Controle
        • Exercício 4
      • Modificadores
      • Eventos
        • Exercício 5
      • Tipos de Referencia
        • Arrays
          • Exercício 6
        • Mappings
          • Exercício 7
        • Structs
          • Exercício 8
      • Address Payable
      • Como os contratos e funções recebem Ether.
      • Transferências de Ether
      • Conceitos Avançados
        • Codificação de ABI
        • Hashing
        • This
        • Herança
        • Abstract
        • Interface
        • Chamada entre contratos
        • EVM
        • ABI
        • Bytecode
        • Opcodes
  • Módulo 3
    • ERCs, Bibliotecas e Padrões de Desenvolvimento
      • Boas Práticas de Desenvolvimento
      • Padrões de Desenvolvimento
      • EIP & ERC
      • ERC-20
      • ERC-721
      • Open Zeppelin
      • Crie um Token ERC-20
      • Almacenamiento Descentralizado: IPFS
      • Crea un Token ERC-721
      • DeFi
  • Módulo 4
    • Kit de ferramentas para desenvolvimento na Ethereum
      • Requisitos para o módulo 4
        • Terminal
        • Git e Github
        • Node.js e npm
        • Visual Studio Code para Solidity
      • Toolkit
        • JSON-RPC
        • Ethers.js
          • Exercício
        • Hardhat
          • Implantação de um contrato no Hardhat
          • Implantação de um contrato em uma rede pública
        • Scaffold-ETH
          • Características do Scaffold-ETHCaracterísticas
          • Como instalar o Scaffold-ETH
  • Módulo 5
    • Segurança, Testes e Auditorias
      • Testes
        • Importância de realizar testes
        • Métodos para testar contratos inteligentes
          • Testes automatizados
          • Testes manuais
        • Conceitos importantes em testes
        • Ferramentas para testes
        • Testes com Hardhat
        • Recursos adicionais
      • Segurança
        • Uma mentalidade diferente de design
        • Principais vulnerabilidades em contratos inteligentes
          • Reentrancy attack (ataque de reentrada)
          • Replay attack (ataque de repetición)
          • Price Oracle Manipulation (Manipulación de Oráculos de Precios)
          • Missing Access Control (Pérdida de Control de Acceso)
          • Reward Manipulation (Manipulación de Recompensas)
          • Failure to Initialize (Falla al Inicializar)
          • Front-running
          • Invariant Breaks (Quebra de Invariantes)
          • Mishandling of ETH (Má gestão de ETH)
          • Denial of Service (DoS - Negação de Serviço)
          • Integer overflow and underflow (Overflow e Underflow de inteiros)
          • Phishing y Typosquatting
        • Recursos adicionais
      • Auditoria de smart contracts
        • Processo de Auditoria
        • Ferramentas
        • Como se preparar para uma auditoria
        • O teste Rekt
        • Desafios
        • Recursos adicionais
  • Contribuye
    • Kipu Explorer
Powered by GitBook
On this page
  1. Módulo 5
  2. Segurança, Testes e Auditorias
  3. Testes

Testes com Hardhat

PreviousFerramentas para testesNextRecursos adicionais

Last updated 22 days ago

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

  1. Devemos ter o Hardhat instalado e um projeto básico criado (caso não saibas como fazer isso, consulte o ).

  2. 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:

    1. Implantar o contrato utilizando o fixture.

    2. Obter o saldo do proprietário.

    3. 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:

    1. Implantar o contrato utilizando o fixture.

    2. Obter o saldo do proprietário.

    3. 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:

    1. Implantar o contrato utilizando o fixture.

    2. Transferir 50 tokens do owner para addr1.

    3. Verificar que addr1 enha recebido 50 tokens.

    4. Transferir 50 tokens de addr1 para addr2.

    5. Verificar que addr2 tenha recebido 50 tokens e que addr1 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:

    1. Implantar o contrato utilizando fixture.

    2. Tentar transferir 1 token de addr1 (que não possui tokens) para o owner.

    3. Verificar que a transação seja revertida com a mensagem "Insufficient balance".

    4. 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:

    1. Implantar o contrato utilizando o fixture.

    2. Transferir 100 tokens do owner para addr1.

    3. Transferir 50 tokens do owner para addr2.

    4. Verificar se o saldo final do owner é igual ao saldo inicial menos 150 tokens.

    5. Verificar se addr1 possui 100 tokens e addr2 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:

  1. Assert: Estilo clássico baseado em funções.

  2. Expect: Estilo BDD (Behavior-Driven Development), mais legível.

  3. 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

  1. 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' });
  1. Booleanos (true, false)

Verifica se um valor é verdadeiro ou falso.

expect(true).to.be.true;
expect(false).to.be.false;
  1. Existência (exist)

Verifica se um valor não é null nem undefined.

let foo = 'bar';
expect(foo).to.exist;
  1. Tipos (a, an)

Verifica o tipo de um valor.

expect('foo').to.be.a('string');
expect({ foo: 'bar' }).to.be.an('object');
  1. 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' });
  1. 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);
  1. 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);
  1. 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);
  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 .

módulo 4
documentação do Hardhat