Testes de Smart Contracts com Foundry

Web3 e Blockchain

Testes de Smart Contracts com Foundry

Luiz Duarte
Escrito por Luiz Duarte em 16/04/2026
Junte-se a mais de 34 mil devs

Entre para minha lista e receba conteúdos exclusivos e com prioridade

Uma das maiores vantagens de você usar um toolkit como o Foundry para apoiar o ciclo de desenvolvimento de projetos de smart contracts é poder contar com testes unitários automatizados. Como em todo tipo de software, buscar uma alta cobertura de testes unitários é metade do caminho para ter uma entrega de qualidade e especificamente no caso de smart contracts, pode ser metade do caminho para evitar um desastre também, já que muitas brechas de segurança podem ser detectadas nesta etapa do desenvolvimento.

No tutorial de hoje, eu vou focar em lhe mostrar os tipos de testes possíveis de serem implementados com o Foundry, fornecendo um guia que lhe auxilie na construção das mais variadas baterias de testes de smart contracts. Para que consiga acompanhar este tutorial você deve ter conhecimentos básicos de smart contracts Solidity, coisa que você aprende neste tutorial, e conhecimentos básicos do toolkit Foundry em si, que você aprende neste outro tutorial. Ajuda também se você souber o básico de TypeScript que ensino aqui.

Vamos lá!

Curso Web3 para Iniciantes

#1 – Ambiente de Testes

Eu não vou escrever com você um projeto novo de smart contract neste tutorial. Ao invés disso, passarei por todos os tipos de testes que já tive a oportunidade de fazer com este toolkit e pretendo ir atualizando este artigo conforme for aprendendo novos “truques”.

Você pode presumir para todos os testes que os contratos estão na pasta src e que nossos testes Solidity estão na pasta test.

No topo do arquivo de testes Solidity temos as seguintes informações:

A versão de Solidity deve ser a mesma do seu contrato, que você vai importar logo abaixo. Por último, importamos o contrato-base dos testes para conseguir fazer os mesmos. Nos testes Solidity nós precisamos extender o contrato Test que importamos e sobrescrever uma função setUp que será chamada automaticamente para inicializar o contrato de testes adequadamente antes de cada função de teste. No exemplo abaixo, eu inicializo um contrato de CRUD de livros e gero dois endereços para os testes (user1 e owner).

Depois, para cada teste, eu escrevo uma função de teste como abaixo.

Nesta função, eu uso a instância do contrato real que inicializamos no setUp para chamar a função a ser testada. Como ela não tem retorno (como a imensa maioria das transações) eu uso uma função leitura no require que valida o teste.

Outra opção em Solidity, que não envolve require, é usando a função utilitária assertEq, onde você passa dois parâmetros que serão comparados para ver se são iguais, como abaixo.

Outra opção é a assertNotEq (para comparar valores diferentes) e a assertTrue (para valores booleanos).

Entendidos estes conceitos fundamentais dos dois ambientes de testes do Foundry, vamos aos cenários de testes.

#2 – Cenários de Sucesso

Primeiro vamos falar dos cenários de sucesso, que são os casos de uso mais comum de serem testados e que buscam verificar se a funcionalidade está, bem, funcionando como deveria.

Testes Simples

O mais comum dos testes é o que chamo de “testes simples“, onde você chama uma função, pega seu retorno e verifica se ele bate com um valor específico, como no exemplo abaixo.

No teste acima, que é do mesmo CRUD de livros que citei antes, verificamos se a quantidade de livros está correta após a adição de um novo. Outra opção seria acessar o mapping de livros pelo id e ver se o title está correto, por exemplo.

Além do require você pode fazer comparações como:

  • assertEq
  • assertNotEq
  • assertTrue
  • tetc

E uma infinidade de operadores para serem aplicados em resultados multivalorados (arrays) também. Você também vai reparar, navegando pelo autocomplete do VS Code vários sinônimos, ou seja, formas iguais de escrever os mesmos testes. Na prática, sendo bem sincero, para cenários de sucesso eu costumo usar apenas o assertEq em 90% dos casos, mas enfim, as opções estão aí.

Testes de Transações (sem eventos)

Existem ocasiões em que você precisa testar transações na blockchain, ou seja, funções que escrevem na mesma e que portanto retornam sempre um recibo de transação e não um valor simples que pode ser comparado. Nesses casos você tem duas opções de como escrever os testes.

O jeito mais simples de conseguir fazer uma asserção que diga se a transação funcionou ou não é chamando uma função de leitura que verifique a variável que foi alterada pela transação, como abaixo, em um teste de transferência de saldo onde usamos a função de balance para ver se os saldos foram alterados. Além disso, o exemplo abaixo mostra a possibilidade de usar múltiplas asserções, algo desejável em diversos cenários em que apenas uma variável não garante que o teste passou 100%.

Note que eu sequer pego o retorno da chamada de transfer, que é a função-alvo do nosso teste. Ao invés disso, eu pego os saldos antes e depois do “from” e do “to” e comparo eles para ver se as quantias estão corretas com um require simples novamente. Isso geralmente é possível de fazer, mas caso você não possua funções de leitura que entreguem o dado atualizado que precisa, o caminho é a segunda opção, que mostrarei logo mais.

Outra opção é usar apenas um require, mas com uma condição booleana composta, usando lógica AND ou OR. No entanto, essa abordagem não permite tanta personalização nas mensagens de erro, por isso dependendo do caso pode ser uma má prática.

Testes de Transações (com eventos)

A segunda opção para testar transações é verificar se um evento foi emitido após a transação e é interessante em dois cenários: no cenário em que você tem essa opção (a função emite eventos) e no cenário que você deseja testar de fato se o evento está sendo emitido corretamente (teste de evento). Independente da situação, o exemplo de código abaixo mostra como testar se um determinado evento foi emitido após uma transação.

Este teste está ligeiramente reduzido pois é de um sistema bem complexo (uma DAO de condomínio), mas ilustra o expect de evento. Aqui a mecânica é ligeiramente diferente para validar eventos. Primeiro, você deve chamar o vm.expectEmit, dizendo se a validação deve considerar os parâmetros do evento, sendo que cada “true” que passei ali são para os parâmetros indexed 1, 2 e 3 (mesmo que não possua 3 indexed, tem de passar os booleanos). Já o quarto booleano são para o “data” do evento (demais parâmetros não indexed). Já o último parâmetro é opcional e serve para quando o evento é emitido por outro contrato, apenas quando há cascateamento de chamadas entre contratos diferentes. Na sequência, você deve emitir o evento pelo contrato de teste, com a exata configuração que será usada na validação, como se fosse para “ensinar” a vm como deve ser o evento correto. Por último, chamamos a função que vai disparar o evento em si.

Caso necessário validar diversos eventos no mesmo teste, você pode repetir o processo para cada um deles, um abaixo do outro.

Curso Beholder
Curso Beholder

#3 – Cenários de Fracasso

Como nem tudo são flores, após escrever os testes de sucesso das suas funcionalidades do smart contract é hora de se preocupar com os cenários de fracasso ou de falha. Nestes cenários, o seu smart contract deve emitir um erro adequado, geralmente usando funções como require, revert e outras.

Teste de Generic Error

Para testar esses erros de execução em Solidity você deve sempre usar o vm.expectRevert. Caso o erro seja emitido usando o tipo genérico, onde você apenas passa uma mensagem, o seu expect se parecerá como abaixo, onde deve informar exatamente a mensagem de erro completa que será emitida.

O vm.expectRevert vai esperar que, na próxima instrução, um erro seja disparado com os bytes especificados. Assim, o require interno da contract.uri(10) vai disparar o erro que vai ser capturado e processado como a validação do teste. Caso a mensagem em si não lhe importe, você pode chamar o expectRevert sem parâmetros.

Abaixo você encontra o trecho de código que dispara o erro anteriormente testado (é um contrato ERC-1155).

Teste de Custom Error

Agora se o erro emitido não é genérico mas sim um erro personalizado (custom error), você deverá usar o também a vm.expectRevert, mas ao invés de passar a mensagem, passamos o selector do custom error existente em algum arquivo Solidity, geralmente o próprio contrato. Na sequência chamamos a função que irá ocasionar o erro.

O único ponto de atenção acima é o import do arquivo onde estão os custom errors e também o encoding correto que varia de custom error para custom error. No caso do ERC20InsufficientBalance, ele pede a conta que disparou o erro (otherAccount), o saldo dela (0) e a quantia que queria transferir (1).

Em 99% dos testes de falhas em smart contracts você usará as duas formas acima.

#4 – Truques em Testes

Para fechar este artigo com chave de ouro, que tal eu passar alguns “truques” que uso na escrita dos meus testes? Nenhum deles é segredo, na verdade alguns provavelmente você já conhece, mas acho que vale a pena serem mencionados aqui para garantir que o conhecimento seja passado adiante.

Impersonate

Primeiro, você deve saber que tanto o deploy quanto a execução dos testes sempre são realizados em nome da conta do contrato de testes, certo? Mas e quando queremos impersonar outra carteira, ou seja, executar uma função em nome de outra conta que não o owner do contrato?

Nos testes em Solidity, este recurso de impersonate pode ser feito através do código abaixo.

O vm.startPrank troca o contexto para o da conta informada até que você chame o vm.stopPrank. Ou seja: a chamada de removeBook que está entre eles será executada como sendo feita pela “otherAccount”.

Querendo fazer “prank” de um bloco de linhas, pode usar a combinação vm.startPrank e vm.stopPrank.

Time

Eventualmente você terá de testar funcionalidades sensíveis ao tempo, como por exemplo cobranças de mensalidades ou timelocks (travas de tempo). Nesses casos você tem de ter um mecanismo que “viaje no tempo” para que a VM de testes esteja em uma data futura entre duas instruções de um teste. Em Solidity você consegue esse efeito usando a vm.warp, uma função que faz a blockchain “viajar” pro futuro x tempo, como no exemplo abaixo onde posicionei o teste 2 dias no futuro.

Funções Auxiliares (Helpers)

Outra dica muito útil é na sua bateria de testes você ter funções auxiliares, ou seja, funções que realizam algumas atividades que auxiliem nos testes. Muitas vezes existem setups específicos de alguns testes ou mesmo este setup pode exigir parametrização diferente para testes diferentes. Como exemplo trago abaixo uma função Solidity auxiliar de uma DAO, onde quero poder adicionar votos em alguns testes para conseguir simular o volume de votos que alguns cenários de teste exigem.

Apenas atenção que o primeiro parâmetro é para mudar o msg.sender, já o segundo (opcional) é para mudar o tx.origin:

Assim, com uma função addVotes eu posso adicionar quantos votos eu quiser e com os parâmetros que quiser em cada teste individual sem comprometer os demais e sem ficar com um monte de linhas de código repetidas na minha bateria de testes.

Saldo

E quando eu preciso de fundos nas contas de teste? Neste caso você pode usar vm.deal para adicionar fundos facilmente.

E estas foram as lições de testes com Foundry de hoje, espero que tenha gostado.

Até a próxima!

TAGS:

Olá, tudo bem?

O que você achou deste conteúdo? Conte nos comentários.

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *