WEB3DEV

Cover image for Teste de Contratos Inteligentes: O Modo Foundry
Panegali
Panegali

Posted on

Teste de Contratos Inteligentes: O Modo Foundry

No artigo anterior, começamos com uma nova ferramenta chamada Foundry para ajudar no ciclo de vida do desenvolvimento de contratos inteligentes. Escrevemos um contrato inteligente do Kickstarter que "mais ou menos" resolve o problema enfrentado pelo projeto Kickstarter atual ao movê-lo para um sistema bastante descentralizado. Você pode encontrar o link para o artigo aqui:

Como a blockchain é projetada para ser imutável, é muito importante que testemos minuciosamente o comportamento do nosso contrato inteligente para ver se ele faz o que é esperado antes de realmente implantá-lo na blockchain. Por favor, note que a necessidade de fazer testes minuciosos em nossos contratos inteligentes não pode ser enfatizado o suficiente. Na verdade, isso é tão importante que as empresas contratam engenheiros de segurança de contratos inteligentes e auditores para analisar seu software em busca de possíveis erros e otimizações necessárias.

O Foundry é uma ferramenta muito poderosa quando se trata de testar contratos inteligentes. Não apenas temos a capacidade de escrever testes em nossa amada linguagem Solidity, mas também vem com alguns "códigos de trapaça" que facilitam muito a escrita de testes. Com o Foundry, podemos ver quantos testes escrevemos e as diferentes áreas do nosso código que o teste cobre muito facilmente. Embora seja bastante inviável escrever testes que cubram 100% do código, devemos pelo menos tentar atingir uma porcentagem muito alta.

Neste artigo, ampliamos nosso conhecimento pré-existente do Foundry ao estender nosso contrato inteligente do Kickstarter do artigo anterior e escrever testes extensivos para ele. Estarei usando o VsCode como meu editor. Vamos direto ao assunto.


Testes básicos

Na minha pasta de testes, vou criar um novo arquivo chamado KickstarterTest.t.sol com o seguinte código:

Como todos os contratos inteligentes com os quais trabalhamos anteriormente, temos nossas declarações e versões habituais. Na linha 5, estamos importando um contrato Base chamado Test do Foundry. Em seguida, nosso contrato KickstarterTest agora "herda" deste contrato de teste e é exatamente isso que o código na linha 7 está fazendo; simplesmente estamos herdando do contrato base de teste do Foundry para que todas as ferramentas de teste estejam prontamente disponíveis para nós.

Com frequência, precisaremos fazer alguma configuração inicial (como financiar uma conta com alguns ethers) antes de executarmos nossos testes. No Foundry, podemos fazer isso usando a funçãosetUp, como visto abaixo.

Função setUp

A função setUp é executada sempre antes de qualquer um dos nossos testes serem realizados. Por exemplo, se tivermos um teste para verificar o saldo de uma conta e outro teste para transferir ethers, então nossa suíte de testes seria executada mais ou menos assim:

setUp() -> testBalance() -> setUp() -> testTransfer()

Para fins de demonstração, vamos escrever apenas um teste simples para nos familiarizarmos com como realmente executamos testes no Foundry.

Nota: eu vou remover este teste quando terminar, ele é apenas para fins de demonstração.

Teste de afirmação

Aqui estamos apenas testando se o valor é realmente 20 usando a função assertEq. Execute o teste usando este comando:

forge test
Enter fullscreen mode Exit fullscreen mode

Você deve ver uma saída como esta:

O que testar...

Agora que vimos alguns testes básicos, temos que testar nosso contrato real do Kickstarter. Uma coisa a saber antes de escrevermos bons testes para nosso contrato (ou qualquer software, na verdade) é: que aspectos do nosso código estamos muito interessados e se esses aspectos devem sempre se comportar adequadamente.

Então, realmente, quais são os aspectos do nosso contrato que nos interessam tanto? Eu notei algumas coisas para as quais podemos absolutamente precisar escrever testes, incluindo (mas não limitados a):

  • O endereço do gerente está correto.
  • A contribuição mínima definida pelo gerente na criação do contrato é realmente a correta.
  • Os usuários que estão financiando não devem fornecer um valor menor do que o valor mínimo de contribuição.
  • Estamos mantendo o controle dos financiadores e quanto eles financiaram até agora.
  • O gerente pode criar solicitações de gastos.
  • Os financiadores não podem criar solicitações de gastos.
  • Os financiadores podem aprovar solicitações de gastos.
  • O gerente não pode finalizar a movimentação do dinheiro sem a maioria dos financiadores aprovar a solicitação de gastos.
  • O gerente pode movimentar o dinheiro se a maioria dos financiadores tiver aprovado a solicitação de gastos.

De maneira alguma isso é tudo o que precisamos testar, isso é apenas suficiente por enquanto. Além disso, para entender completamente o que estamos planejando testar, sugiro que você faça uma revisão rápida do artigo anterior onde trabalhamos no contrato do Kickstarter, caso ainda não o tenha feito. O link está aqui.

Um pouco de refatoração de código...

Antes de escrever nosso teste, há uma pequena modificação que precisaremos fazer em nossa implementação anterior. O Foundry nos fornece uma maneira de escrever scripts de implantação que simulam a implantação em várias redes blockchain. Então, você adivinhou certo, vamos criar nosso próprio script de implantação.

Primeiro, vamos excluir o arquivo Deploy.sol que contém nossa fábrica DeployCampaign do artigo anterior e, em seguida, alterar o construtor do contrato Campaign para aceitar apenas a contribuição mínima.

Na pasta de scripts, crie um arquivo DeployKickstarter.s.sol com o seguinte conteúdo:

Importamos nossa campanha, em seguida, criamos um contrato DeployCampaign que herda do Script base do Foundry. Então, criamos uma função run que retorna uma campanha. A função run é o ponto de entrada do nosso script de implantação. As funções vm.startBroadcast() e vm.stopBroadcast() são códigos de trapaça que vêm com scripts do Foundry, basicamente, eles transmitem a transação para implantar o contrato em uma blockchain.

Usamos a função setAmount para definir o valor mínimo e, entre o início e o fim da transmissão, criamos nossa campanha e passamos o valor mínimo. Novamente, isso implanta o contrato na blockchain. Execute o comando:

forge script script/DeployKickstarter.s.sol

Enter fullscreen mode Exit fullscreen mode

Você deverá ver saídas semelhantes a estas:

A vantagem de criar scripts de implantação como este é que não apenas os usamos para implantação, mas também para testes e temos certeza de que temos a mesma implantação configurada em ambos os cenários.

Agora podemos usar este script de implantação em nossa função de configuração de teste da seguinte maneira:

teste setUp

Não há muita novidade aqui, apenas criamos uma constante para armazenar o valor mínimo que será usado durante nosso teste. Na função setUp, criamos um deployCampaign, definimos o valor como o valor mínimo, executamos a implantação e salvamos a campanha retornada em nosso armazenamento de campanha.

Testes reais

Certo, finalmente chegamos ao ponto em que podemos começar a escrever testes para nossa campanha. Primeiro, vamos testar se nosso gerente é o correto.

Você provavelmente está se perguntando por que o endereço usado é o msg.sender e não o endereço do deployCampaign. Outra coisa que o start e stop broadcast faz é que ele define o remetente real da transação como o remetente, e não o deployCampaign.

Se tudo foi feito corretamente, você deverá ver que o teste passa assim:

Ótimo, até agora tudo bem. Vamos para o próximo teste.

Em seguida, testamos se o valor mínimo é o valor correto.

Em seguida, execute o teste do forge novamente. Você deverá ver 2 testes aprovados.

Então, testamos se um financiador não pode contribuir com menos do que o valor mínimo.

O vm.expectRevert() significa que o código abaixo dele deve reverter. Como estamos passando um valor menor do que o valor mínimo de contribuição, estamos simplesmente dizendo que esperamos que as linhas abaixo dele falhem. Execute os testes novamente e eles também devem passar.

Em seguida, testamos se estamos mantendo o controle dos financiadores e do valor que eles financiaram.

Ao escrever testes, às vezes pode ser difícil saber quem está enviando uma transação específica, razão pela qual o Foundry fornece um código de trapaça que nos permite declarar explicitamente quem será o remetente das transações.

Antes de continuar com nossos testes, vamos criar um usuário que será responsável por enviar todas as nossas transações.

O Forge nos fornece uma função makeAddr que aceita um nome e retorna um endereço. Este será o endereço do usuário que enviará nossas transações.

Adicionei uma função ao nosso contrato de campanha no arquivo Kickstarter.sol para obter o valor financiado por um financiador.

Podemos então usar esta função em nosso teste da seguinte forma:

Execute este teste com a flag -vvvv. Você deverá ver seus testes falhando.

Observe o erro EvmError na parte inferior é "Out of fund" (Sem fundos). Significa que o usuário não possui ethers suficientes para a transação e especificamente possui zero ethers. Isso é esperado, já que apenas criamos o usuário sem fornecer algum saldo inicial para sua conta. Felizmente para nós, o forge também fornece outro código de trapaça que dá alguns ethers ao nosso usuário. Agora vamos magicamente produzir ethers do nada.

A flag -vvvv usada é uma espécie de nível de registro onde o número de v's varia de 1 a 5 e cada um mostra mais registros para o seu teste. Quanto mais v, mais registros abrangentes você vê.

Em nossa função setUp, vamos dar ao nosso usuário 50 ethers.

Código de trapaça vm.deal()

Modifique seu teste novamente como abaixo, execute-o e eles devem estar passando agora.

Código de trapaça vm.prank()

O vm.prank(USER) significa que a próxima transação imediata será enviada pelo USUÁRIO.


Eu sei que foram muitos códigos de trapaça, mas vamos continuar com nosso teste. Em seguida, devemos testar se um gerente pode criar uma solicitação de gasto. Adicionei o seguinte código ao contrato da campanha e o usei em nosso teste abaixo.


function getSpendRequestCount() public view returns (uint) {
    return s_numRequests;

}

Enter fullscreen mode Exit fullscreen mode

Criamos um destinatário e o pré-financiamos com 5 ethers. Este é o endereço para o qual o gerente está tentando enviar o dinheiro para a solicitação de gasto. Adicionamos um usuário à Campanha e criamos a solicitação de gasto, declarando explicitamente que o msg.sender (gerente) é o remetente da transação e afirmamos que agora temos 1 solicitação de gasto criada.

Observe que o vm.startPrank usado acima é essencialmente a mesma coisa que vm.prank. vm.prank apenas especifica a próxima transação imediata, enquanto vm.startPrank especifica que tudo entre o início e o fim será enviado pelo usuário.

Em seguida, testamos que o gerente não pode finalizar uma solicitação de gasto se a maioria dos financiadores não a tiver aprovado.

Esperamos um retorno quando chamamos a função finalizeRequest pela primeira vez para a primeira solicitação de gasto criada quando ela não foi aprovada.

Finalmente, vamos testar se uma solicitação de gasto que foi aprovada pela maioria pode ser finalizada pelo gerente. Adicionei outra função getter ao contrato Campaign que se parece com o seguinte e foi usada nos meus testes:

function getSpendRequestCompletedStatus(
 uint index
) public view returns (bool) {
 return s_spendRequests[index].complete;
}
Enter fullscreen mode Exit fullscreen mode

Mais uma vez, criamos uma campanha, verificamos se não foi concluída, então nosso financiador aprovou, o gerente finalizou, enviando os ethers para o endereço desejado. Em seguida, verificamos se a solicitação de gasto foi concluída e se o saldo do destinatário aumentou.

Ótimo, até agora escrevemos testes muito extensivos para nosso contrato do Kickstarter e ainda há algumas áreas para testar, mas deixarei isso para você estender os casos de teste existentes. Na verdade, podemos ver quanto do nosso código o teste cobre executando o comando:

forge coverage
Enter fullscreen mode Exit fullscreen mode

Uau, conseguimos escrever testes que cobrem cerca de 96% do nosso contrato do Kickstarter. Isso é bastante bom por enquanto.

Você pode encontrar o código para o conjunto completo de testes abaixo:


// SPDX-License-Identifier: MIT

pragma solidity ^0.8.21;

import {Test, console} from "forge-std/Test.sol";

import {DeployCampaign} from "../script/DeployKickstarter.s.sol";

import {Campaign} from "../src/Kickstarter.sol";

contract KickstarterTest is Test {

    uint constant MINIMUM_AMOUNT = 50;

    Campaign campaign;

    address USER = makeAddr("ifeoluwa");

    uint constant INITIAL_BALANCE = 50 ether;

    function setUp() external {

        DeployCampaign deployCampign = new DeployCampaign();

        deployCampign.setAmount(MINIMUM_AMOUNT);

        campaign = deployCampign.run();

        vm.deal(USER, INITIAL_BALANCE);

    }

    function testManagerAddress() public {

        assertEq(campaign.getManager(), msg.sender);

    }

    function testMinimumAmountIs20() public {

        assertEq(campaign.getMinimumContribution(), MINIMUM_AMOUNT);

    }

    function testCannotContributeLessThanMinimumAmount() public {

        vm.expectRevert();

        campaign.contribute{value: 20}();

    }

    function testFundersAndAmountFunded() public {

        vm.prank(USER);

        campaign.contribute{value: 500}();

        assertEq(campaign.getFunderAmount(USER), 500);

        vm.prank(USER);

        campaign.contribute{value: 200}();

        assertEq(campaign.getFunderAmount(USER), 700);

    }

    function testManagersCanCreateSpendRequest() public {

        address RECIPIENT = makeAddr("recipient");

        vm.deal(RECIPIENT, 5 ether);

        assertEq(campaign.getSpendRequestCount(), 0);

        vm.startPrank(USER);

        campaign.contribute{value: 200000}();

        vm.stopPrank();

        vm.prank(msg.sender);

        campaign.createSpendRequest(

            "some description",

            10000,

            payable(RECIPIENT)

        );

        assertEq(campaign.getSpendRequestCount(), 1);

    }

    function testCannotFinalizeIfNotApproved() public {

        address RECIPIENT = makeAddr("recipient");

        vm.startPrank(USER);

        campaign.contribute{value: 200000}();

        vm.stopPrank();

        vm.prank(msg.sender);

        campaign.createSpendRequest(

            "some description",

            10000,

            payable(RECIPIENT)

        );

        vm.expectRevert();

        vm.prank(msg.sender);

        campaign.finalizeRequest(0);

    }

    function testCanFinalizeIfApproved() public {

        address RECIPIENT = makeAddr("recipient");

        vm.startPrank(USER);

        campaign.contribute{value: 200000}();

        vm.stopPrank();

        vm.startPrank(msg.sender);

        campaign.createSpendRequest(

            "some description",

            10000,

            payable(RECIPIENT)

        );

        vm.stopPrank();

        assertEq(campaign.getSpendRequestCompletedStatus(0), false);

        vm.startPrank(USER);

        campaign.approveRequest(0);

        vm.stopPrank();

        vm.prank(msg.sender);

        campaign.finalizeRequest(0);

        assertEq(campaign.getSpendRequestCompletedStatus(0), true);

        assertEq(RECIPIENT.balance, 10000);

    }

}

Enter fullscreen mode Exit fullscreen mode

Isso seria tudo para este artigo, espero que tenha sido útil, pois explica os conceitos básicos de teste com o Foundry e algumas das ferramentas e truques fornecidos ao escrever testes. Por favor, curta, compartilhe, comente e siga. Nos vemos no próximo, até logo!


Artigo escrito por Ifeoluwaolubo. Traduzido por Marcelo Panegali.

Oldest comments (0)