Foundry: A Arte de Testar em Solidity

Patrick (Barba) Carneiro
bellum-galaxy-community
5 min readMay 21, 2024

Após 23 dias do início do hackathon Block Magic, estamos caminhando para o seu encerramento. Foi um período de imenso aprendizado, desafios, debates e muito trabalho. Os times estão na reta final de preparação, com a cabeça no pitch, na documentação e na gravação da demonstração da aplicação. Agora, não resta tempo para sustos inesperados. Mas como garantir isso?

Imagem gerada por AI.

Testando com Foundry

Foundry é o melhor framework para desenvolvimento em Solidity. É uma ferramenta muito completa, que permite realizar desde testes unitários até os famigerados testes Stateful Fuzzing.

Hoje, como mentor, acompanhando-os desde o@Chainlink Smart Contract Bootcamp, sei que muitos de vocês estão iniciando no desenvolvimento Web3 e, portanto, estão mais preocupados em desenvolver do que em testar, e isso é muito normal. No entanto, isso pode gerar problemas, principalmente considerando o pouco tempo até o final do hackathon.

Estou aqui para ajudá-los a estarem à frente de possíveis erros de lógica que podem comprometer a entrega do projeto e trago meios para que vocês possam testar seus projetos de forma rápida e fácil, apenas codando em Solidity. Isso os ajudará a evoluir como desenvolvedores Solidity.

Preparando Testes

Quando iniciamos um workspace com Foundry a partir do comando forge init, o Foundry nos dá algumas cascas de contratos que podem ser usadas. Como sabemos, os contratos em Solidity têm a extensão .sol. Mas e os testes e scripts de deploy?

Em Solidity, os contratos onde escrevemos os scripts de deploy e testes são nomeados com as extensões .s.sol e .t.sol, respectivamente. Isso ajuda o Foundry a diferenciar cada contrato, visto que tudo é escrito em Solidity.

Como nosso foco aqui são os testes e não dependemos necessariamente dos scripts para testar, irei deixá-los de lado para outra conversa. Mas tenha ciência de que os scripts são muito úteis.

Os contratos de teste, além da extensão em seu nome, precisam de algumas ferramentas específicas que o Foundry nos fornece. Veja no mock a seguir:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

//Esses são imports da biblioteca do Foundry que nos ajudam com os testes.
//Test
//é a 'maleta de ferramentas' do Foundry que nos fornece tudo que precisamos
//para realizar os testes em nossos contratos.
//console2
//é uma ferramenta para nos ajudar a debuggar logando valores
//exatamente como no JavaScript
import {Test, console2} from "forge-std/Test.sol";

//Foundry também nos fornece alguns mocks como ERC20 e ERC721
//Mas é preferível utilizar os mocks da OpenZeppelin
import {ERC20Mock} from "@openzeppelin/contracts/mocks/token/ERC20Mock.sol";

//Essa é a importação do seu contrato que será testado
import {MeuContrato} from "../src/MeuContrato.sol";

//Note que para usar as ferramentas, nós precisamos
//declarar nosso contrato como `is Test`, a biblioteca importada
contract ProjetoUnitTest is Test {

//Para realizar os testes, precisaremos simular endereços. Certo?
//O foundry nos permite criar endereços de várias maneiras.
//As mais comuns são:
address fakeOne = address(1);
address BellumGalaxy = makeAddr("BellumGalaxy");

//É sempre importante usar constantes para iniciar valores
//Isso facilita para que você mantenha um padrão através de todos os testes
uint256 constant INITIAL_BALANCE = 10 ether;

//Para realizar o deploy do seu contrato ou dos mocks
function setUp() public {
//Se existir algum parâmetro no construtor
//devemos passá-los entre os parênteses
MeuContrato contrato = new MeuContrato();
//Ou, no caso do Mock, assim:
ERC20Mock erc20 = new ERC20Mock();

//A partir disso, podemos mintar tokens para usar nos testes
erc20.mint(BellumGalaxy, INITIAL_BALANCE);
//De acordo com nosso projeto, podemos precisar de Ether. Certo?
//Foundry nos permite gerar ether a partir do seguinte comando
vm.deal(BellumGalaxy, INITIAL_BALANCE);
}

Para instalar uma biblioteca como OpenZeppelin no seu ambiente de trabalho e testes, basta usar

forge install https://github.com/OpenZeppelin/openzeppelin-contracts --no-commit

Acessar o foundry.toml e adicionar o remapping assim:

remappings = ["@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/"]

Espero que estejam acompanhando até aqui. Agora a brincadeira começa a ficar bem mais interessante, pois finalmente entraremos nos testes!

Executando Testes

Vamos dar continuidade ao nosso contrato e aprender algumas ferramentas que o Foundry nos fornece para validar eventos, storage, reverts, etc.

    //Verificar o saldo de ether de uma carteira
function test_verifyingAddressBalance() public {
vm.deal(BellumGalaxy, INITIAL_BALANCE);
//Durante testes, é muito comum executar transações que custam ether
//Então é comum checar os valores para confirmar que tudo está ocorrendo como esperado.
//Para fazer isso, usamos .balance
uint256 bgBalance = BellumGalaxy.balance;
//E nós podemos usar o console para debuggar valores.
console2.log(bgBalance);
}

//Verificar valores
function test_assertingValues() public {
uint256 expectedValue = 2;
uint256 givenValue = 1+1;
//Através dos testes, nós precisamos validar resultados automaticamente
//Geralmente não temos tempo para ficar logando tudo e verificando manualmente
//Foundry nos fornece ferramentas como:
//assertEq() compara dois valores, do mesmo tipo, que devem ser equivalentes
assertEq(expectedValue, givenValue);
//assertTrue() é um verdadeiro ou falso para validar uma declaração específica
assertTrue(2 > 1);
}

//Verificar a emissão de eventos
event CounterTest_ExampleEvent(uint256 counter);
function test_emitEvent() public {
uint256 expectedValue = 1;

//Ferramenta do Foundry
vm.expectEmit();
//Declara a emissão do evento e o parâmetro esperado
emit CounterTest_ExampleEvent(expectedValue);
//Chama a função que deve emitir o evento declarado acima.
counter.setNumber(0);
}

//Verificar a emissão de Erros Customizados
error CounterTest_InputValueTooLow();
function test_emitCustomError() public{
//Essa é uma ferramenta que nos permite checar reverts
//Se o Erro Customizado retornar um parâmetro
//Basta adicionar uma virgular e informar o parametro após o .selector
vm.expectRevert(abi.encodeWithSelector(CounterTest_InputValueTooLow.selector));
//Chama a função que deve reverter e emitir o erro
counter.setNumber(0);
}

//Verificar a emissão de erros comuns
function test_emitCommonError() public{
//Em caso de erros comuns, só precisamos passar a string:
vm.expectRevert("Caller is not the owner");
//E chamar a função que deve reverter e emitir o erro.
counter.setNumber(0);
}
}

Conclusão

Independentemente do objetivo do seu projeto, lembre-se de que uma suíte de testes robusta é essencial. Utilizar uma ferramenta como Foundry não só facilita o processo de teste, mas também fortalece a segurança e a confiabilidade do seu código. Escrever testes permite identificar e eliminar vulnerabilidades que poderiam causar danos significativos ao seu projeto, seja ele um projeto de hackathon ou uma aplicação em uma startup. Invista tempo nos testes e colha os benefícios de um desenvolvimento mais seguro e eficiente.

Precisa de orientação para melhorar a qualidade dos seus testes, preparar o seu projeto para uma auditoria, ou está procurando por um auditor independente?
Entre em contato comigo.

Conecte-se com a Bellum Galaxy:

Visite nosso site, junte-se ao nosso Discord, siga-nos no X, Instagram e no LinkedIn para ficar por dentro de nossas aventuras e insights.

--

--

Patrick (Barba) Carneiro
bellum-galaxy-community

Solidity Developer | Security Researcher | Chainlink Developer Expert | @bellumgalaxy Founder