WEB3DEV

Cover image for Teste Unitário com Truffle e Celo
Diogo Jorge
Diogo Jorge

Posted on

Teste Unitário com Truffle e Celo

Image description

Introdução

Ao criar aplicativos descentralizados que utilizam contratos inteligentes, é importante garantir que haja pouca ou nenhuma vulnerabilidade para impedir que um invasor comprometa seu aplicativo.

O teste unitário ajuda a garantir que todas as funcionalidades em seu contrato estejam funcionando conforme o esperado. E ambientes de desenvolvimento como o Truffle fornecem as mesmas ferramentas para ajudá-lo a escrever testes proficientes para seus contratos antes da implantação final.

Neste tutorial, você criará um contrato exemplar e aprenderá como escrever e executar testes unitários para seu contrato usando o ambiente de desenvolvimento Truffle.

Pré-requisitos

Ao longo deste tutorial, você precisará ter trabalhado ou ter um conhecimento básico do seguinte:

  • Suíte Truffle: A Suíte Truffle é um ambiente de desenvolvimento que atua como um canal para interagir com a EVM e também fornece recursos essenciais e bibliotecas valiosas para testar contratos inteligentes na Ethereum e facilitar a interação com a blockchain.
  • Solidity: Solidity é uma linguagem de programação de alto nível usada para criar contratos inteligentes.
  • Javascript: Este tutorial fará uso do Javascript, portanto, você deve estar familiarizado com a codificação e os algoritmos básicos do Javascript.

Requisitos

Este tutorial também assume que você já tenha o seguinte instalado ou disponível:

  • Gerenciamento de nós e pacotes de nós npm ou yarn: este tutorial exigirá que você use um gerenciador de pacotes de nó pré-instalado. Você também deve saber como trabalhar com qualquer gerenciador de pacotes: npm ou yarn.

Instalando e configurando a Suíte Truffle

Para instalar a Suíte Truffle usando seu terminal, crie um espaço de trabalho, vá até o diretório em seu terminal e execute o comando npm install -g truffle.

Agora, execute o comando npx truffle init para ativar o ambiente de desenvolvimento. Você notará que uma nova estrutura de arquivos aparece em seu explorador de arquivos, algo como a imagem abaixo:

Image description

Executando uma simulação de teste de contrato

Para entender como funciona o teste unitário usando a Suíte Truffle, crie um diretório demo, diferente do seu diretório principal, e execute o comando npx truffle unbox metacoin. O resultado da execução bem-sucedida do código deve ser semelhante à imagem abaixo.

Image description

O comando inicia um projeto de demonstração chamado <metacoin> incluindo dois arquivos de contrato MetaCoin.sole ConvertLib.sol no diretório do contrato e também dois arquivos de testeTestMetaCoin.sol e metacoin.js no diretório de teste, para executar testes unitários nos contratos de metacoin.

Agora execute o comando npx truffle teste o resultado do teste unitário deve ser exatamente como na imagem abaixo.

Image description

O Truffle primeiro compila o contrato, executa todos os testes de unidade no script de teste e retorna o resultado de todos os testes. A imagem acima mostra o resultado quando passa em todos os testes unitários.

Criando o contrato inteligente

Cada teste de contrato realizado é composto explicitamente para testar um contrato específico, ou seja, se você tiver quatro arquivos de contrato diferentes em um aplicativo, seu aplicativo também deverá ter quatro scripts de teste para testar cada contrato. Nas etapas a seguir, você escreverá um modelo de contrato simples para o qual, posteriormente, escreverá um teste.

Observação: se você é novo em Solidity e na criação de contratos inteligentes, confira esse tutorial para começar e entender o código Solidity. O tutorial acima também possui algumas funções que o ajudarão a aprender como escrever código Solidity.

  1. Volte para o diretório inicial do ambiente de desenvolvimento que você criou; dentro da pasta do contrato, crie um novo arquivo, Sample.sol. Este será o contrato inteligente para o qual você escreverá testes unitários.
  2. O Contrato Sample.solterá as seguintes funcionalidades:
  • a. Primeiro, o contrato é criado e as variáveis name e age também são criadas e, por padrão, não têm valor.
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.17;

contract Sample {
   string public name;
   address public owner;

}

Enter fullscreen mode Exit fullscreen mode
  • b. Em seguida, a função construtora do contrato atribui o endereço do implementador do contrato à variável owner e atribui a string "deployer" à variável name.
constructor() {
      owner = msg.sender;
      name = "deployer";
  }

Enter fullscreen mode Exit fullscreen mode
  • c. A próxima função rename aceita um valor de string como argumento e o atribui à variável name.
 function rename(string memory _name) public {
       name = _name;
   }

Enter fullscreen mode Exit fullscreen mode
  • d. A próxima função describe simplesmente retorna os valores atuais da variável global, name.
function describe() public view returns (string memory) {
       return (name);
   }

Enter fullscreen mode Exit fullscreen mode
  • e. Em seguida tem uma função modificadora ownerOnly que permite apenas que o proprietário do contrato chame sua função geradora quando adicionado a qualquer função.
modifier ownerOnly() {
       require(
           msg.sender == owner,
           "Esta função requer o proprietário do contrato para executar"
       );
       _;
   }

Enter fullscreen mode Exit fullscreen mode
  • f. A seguinte função changeOwner usa o modificador criado anteriormente ownerOnly para permitir apenas que o proprietário do contrato altere a função do proprietário do contrato para qualquer endereço passando como um argumento para a função changeOwner.
   function changeOwner(address _newOwner) public ownerOnly {
       owner = _newOwner;
   }

Enter fullscreen mode Exit fullscreen mode
  • g. A próxima função deposit permite que qualquer pessoa envie um mínimo de 1 ETH ao contrato.
function deposit() public payable {
       require(
           msg.value >= 0.01 * 10 ** 18,
           "você precis enviar pelo menos 0.01 ETH"
       );
   }

Enter fullscreen mode Exit fullscreen mode
  • h. Por fim, a última função do contrato Sample.sol permite que qualquer pessoa que chame o contrato retire fundos do contrato, desde que você passe o número de tokens para retirar como argumento. Esta transação também será encerrada se o valor repassado exceder 10 ETH.
function withdraw(uint256 _amount) public payable {
       require(_amount <= 100000000000000000);
       payable(msg.sender).transfer(_amount);
   }
Enter fullscreen mode Exit fullscreen mode

Se você concluiu seu contrato Sample.sol, seu contrato inteligente deve ser exatamente como o código abaixo. Você deve atualizar seu contrato com o código abaixo para uniformidade:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.17;

contract Sample {
   string public name;
   address public owner;

   constructor() {
       owner = msg.sender;
       name = "deployer";
   }

   function rename(string memory _name) public {
       name = _name;
   }

   function describe() public view returns (string memory) {
       return (name);
   }

   modifier ownerOnly() {
       require(
           msg.sender == owner,
           "Esta função requer o proprietário do contrato para executar"
       );
       _;
   }

   function changeOwner(address _newOwner) public ownerOnly {
       owner = _newOwner;
   }

   function deposit() public payable {
       require(
           msg.value >= 0.01 * 10 ** 18,
           "você precisa enviar pelo menos 0.01 ETH"
       );
   }

   function withdraw(uint256 _amount) public payable {
       require(_amount <= 100000000000000000);
       payable(msg.sender).transfer(_amount);
   }
}
Enter fullscreen mode Exit fullscreen mode

Para confirmar que não há erros existentes em seu contrato, execute o comando npx truffle compile em seu terminal, e um resultado bem-sucedido deve se parecer com a imagem abaixo.

Image description

Agora que você conhece as diferentes funções do contrato Sample.sole você está familiarizado com o que eles fazem. A seguir, você aprenderá como criar um script de teste unitário para testar subseções do contrato que acabou de fazer.

Escrevendo o script de teste unitário

Agora que você criou o contrato Sample.sol, você pode começar a escrever os testes unitários para o contrato. Depois de concluir esses testes, você terá uma ideia básica de como criar testes unitários para contratos inteligentes.

Um padrão muito comum usado ao escrever testes unitário para contratos inteligentes é:

a. Arrange (Arranjar): é aqui que você cria variáveis ​​fictícias que precisará para executar unidades de seus casos de teste. Eles podem ser criados globalmente após a criação da função de teste de contrato ou localmente dentro do teste unitário.

b.Act (Agir): Em seguida, vem a parte em que você executa suas funções de teste e armazena o resultado em uma variável.

c.Assert (Afirmar): Como você já sabe o resultado correto do teste, compare o resultado esperado com a resposta do teste executado. Se o teste retornar o resultado esperado, ele passa, caso contrário, o teste não passa.

Também seguindo o formato:

describe(<"functionName">, async function () {
 beforeEach(async function() {
<o que deve acontecer antes de cada teste ser executado>
})
   it("o que se espera que o teste faça", async function () {
     const response = <o que foi retornado>
     const result = <o que deveria ser retornado>;
     expect(response).to.equal(result); // compara a resposta com o resultado esperado
   });
Enter fullscreen mode Exit fullscreen mode

Em seguida, você criará um uint-test para testar seu contrato Sample.sol usando o formato anterior acima e você aprenderá como criar um script básico de teste unitário por conta própria:

Testar um contrato inteligente facilita a identificação de bugs e vulnerabilidades e reduz a possibilidade de erros de software que podem levar a explorações dispendiosas. Nas próximas etapas, você aprenderá o formato básico de como escrever testes unitários com base em seu contrato inteligente.

  • Primeiro, dirija-se à pasta migrations e crie um arquivo de script chamado 1_deploy_contract.js e copie o código abaixo no script.
const Sample = artifacts.require("Sample");
// const MetaCoin = artifacts.require("MetaCoin");

module.exports = function (deployer) {
 // deployer.deploy(Sample);
 // deployer.link(Sample, SampleTest);
 // deployer.deploy(SamplTest);
 deployer.deploy(Sample, { gas: 1000000 });
};
Enter fullscreen mode Exit fullscreen mode

O código acima foi criado simplesmente para implantar seu contrato Sample.sol. Em seguida, navegue até a pasta teste e crie um novo script de teste, SampleTest.js.

1.Em primeiro lugar, você precisará importar o contrato como uma variável Sample na primeira linha do código.

const Sample = artifacts.require("Sample");

Enter fullscreen mode Exit fullscreen mode

2.Em seguida, você precisará inicializar o teste de contrato com o seguinte código abaixo. Este contrato - Amostra cobrirá todas as funções de teste unitário que serão executadas no contrato nomeado.

contract("Sample", (accounts) => {
})

Enter fullscreen mode Exit fullscreen mode

3.Usando a palavra-chave describe para definir um teste específico para cada função do contrato, você pode realizar vários testes usando a palavra-chave it para uma função específica. O primeiro teste constructor testa a função do construtor no contrato. Copie e adicione o código abaixo.

 describe("constructor", async function () {
   it("deve ter o nome correto", async () => {
     const sample = await Sample.deployed();
     const name = await sample.name();
     assert.equal(name, "deployer");
   });

   it("deve ter o proprietário correto", async () => {
const sample = await Sample.deployed();
     const owner = await sample.owner();
     assert.equal(owner, accounts[0]);
   });
 });
Enter fullscreen mode Exit fullscreen mode

A função tem dois testes com descrições de string do que cada um deles deve fazer. O primeiro teste verifica a inicialização da variável name e verifica o valor da variável proprietária para o endereço do implantador. O teste passa se o resultado retorna como esperado e reverte com um erro caso contrário.

Agora, execute o comando npx truffle test, e um resultado bem-sucedido deve se parecer com a imagem abaixo.

Image description

  1. O próximo teste unitário descreve a função rename e describe do contrato inteligente; a função realiza um único teste na função rename e describe. O teste atualiza o valor da variável name e verifica se o valor atual da variável foi atualizado. Copie e adicione o código abaixo.
 describe("renomear e descrever", async function () {
   it("deve ser capaz de renomear", async () => {
     const sample = await Sample.deployed();
     await sample.rename("new name");
     const name = await sample.describe();
     assert.equal(name, "new name");
   });
 });
Enter fullscreen mode Exit fullscreen mode

Agora, execute o comando npx truffle test e um resultado bem-sucedido deve se parecer com a imagem abaixo.

Image description

  1. O próximo teste unitário descreve a função changeOwner no contrato inteligente; o teste primeiramente usa o endereço correto para tentar alterar o proprietário, que deve passar com sucesso. E então usa outro endereço aleatório para alterar a função de propriedade, que deve ser revertida. Copie e adicione o código abaixo.
describe("changeOwner", async function () {
   it("deve mudar o proprietário", async () => {
     const sample = await Sample.deployed();
     await sample.changeOwner(accounts[1], { from: accounts[0] });
     const owner = await sample.owner();
     assert.equal(owner, accounts[1]);
   });

   it("não deve mudar o proprietário", async () => {
     const sample = await Sample.deployed();
     try {
       await sample.changeOwner(accounts[2], { from: accounts[1]});
     } catch (error) {
       assert.equal(
         error.message,
         "Exceção de VM ao processar transação: reverter"
       )};
   });
 });
Enter fullscreen mode Exit fullscreen mode

Agora, execute o comando npx truffle test e um resultado bem-sucedido deve se parecer com a imagem abaixo.

Image description

  1. A próxima função testa a função deposit do contrato. O primeiro teste verificará se a função de depósito funciona corretamente, o que permite depósitos de 0,01 ETH ou mais. O segundo teste verifica se a função de depósito rejeita corretamente depósitos inferiores a 0,01 ETH. Copie e cole o código abaixo.
describe("deposit", async function () {
   it("deve permitir depósitos", async () => {
     const sample = await Sample.deployed();
     await sample.deposit({ value: 0.01 * 10 ** 18 });
   });
   it("não deve permitir depósitos abaixo de 0.01 ETH", async () => {
     const sample = await Sample.deployed();
     try {
       await sample.deposit({ value: 0.009 * 10 ** 18 });
       assert.fail("o depósito deveria ter falhado");
     } catch (error) {
       assert.ok(error.message.includes("revert"));
     }
   });
 });
Enter fullscreen mode Exit fullscreen mode

Agora, execute o comando npx truffle test e um resultado bem-sucedido deve se parecer com a imagem abaixo.

Image description

  1. A próxima função describe testa a função withdrawno contrato. O primeiro teste é tentar sacar 0,01 ether do contrato. O segundo teste é tentar sacar uma quantia maior que o saldo para garantir que a retirada falhe. Se o teste falhar, ele retornará uma mensagem de erro com a palavra revert.
describe("withdraw", async function () {
   it("deve permitir retiradas", async () => {
     const sample = await Sample.deployed();
     await sample.withdraw(BigInt(0.01 * 10 ** 18));
   });
   it("não deve permitir retiradas acima do saldo", async () => {
     const sample = await Sample.deployed();
     try {
       await sample.withdraw(BigInt(0.01 * 10 ** 18));
       assert.fail("retirada deve falhar");
     } catch (error) {
       assert.ok(error.message.includes("revert"));
     }
   });
 });
Enter fullscreen mode Exit fullscreen mode

Por fim, execute o comando npx truffle test e um resultado bem-sucedido deve se parecer com a imagem abaixo.

Image description

Depois de concluir seu script de teste, seu código deve ficar exatamente como o abaixo. Para manter uniformidade, substitua todo o código por este teste de código.

const Sample = artifacts.require("Sample");

contract("Sample", (accounts) => {
 describe("constructor", async function () {
   it("deve ter o nome correto", async () => {
     const sample = await Sample.deployed();
     const name = await sample.name();
     assert.equal(name, "deployer");
   });

   it("should have the correct owner", async () => {
     const sample = await Sample.deployed();
     const owner = await sample.owner();
     assert.equal(owner, accounts[0]);
   });
 });

 describe("rename & describe", async function () {
   it("deve ser capaz de renomear", async () => {
     const sample = await Sample.deployed();
     await sample.rename("new name");
     const name = await sample.describe();
     assert.equal(name, "new name");
   });
 });
 describe("changeOwner", async function () {
   it("deve mudar o proprietário", async () => {
     const sample = await Sample.deployed();
     await sample.changeOwner(accounts[1], { from: accounts[0] });
     const owner = await sample.owner();
     assert.equal(owner, accounts[1]);
   });

   it("não deve mudar o proprietário", async () => {
     const sample = await Sample.deployed();
     try {
       await sample.changeOwner(accounts[2], { from: accounts[1] });
     } catch (error) {
       assert.equal(
         error.message,
         "Exceção VM enquanto processa a transação: reverter"
       );
     }
   });
 });
 describe("deposit", async function () {
   it("deve permitir depósitos", async () => {
     const sample = await Sample.deployed();
     await sample.deposit({ value: 0.01 * 10 ** 18 });
   });
   it("não deve permitir depósitos abaixo de 0.01 ETH", async () => {
     const sample = await Sample.deployed();
     try {
       await sample.deposit({ value: 0.009 * 10 ** 18 });
       assert.fail("deposit should have failed");
     } catch (error) {
       assert.ok(error.message.includes("revert"));
     }
   });
 });
 describe("withdraw", async function () {
     it("deve permitir retiradas", async () => {
       const sample = await Sample.deployed();
       await sample.withdraw(BigInt(0.01 * 10 ** 18));
     });
     it("não deve permitir retiradas acima do saldo", async () => {
       const sample = await Sample.deployed();
       try {
         await sample.withdraw(BigInt(0.01 * 10 ** 18));
         assert.fail("retirada deve ter falhado");
       } catch (error) {
         assert.ok(error.message.includes("revert"));
       }
     });
   });
});
Enter fullscreen mode Exit fullscreen mode

Conclusão

Escrever testes unitários para contratos inteligentes pode ajudar muito a garantir um contrato seguro e eficiente, sugerindo correções e melhorias depois de descobrir erros, problemas e vulnerabilidades de segurança em seu contrato. Você criou com sucesso seu script de teste unitário para um contrato de amostra simples usando Truffle. Agora que você entende como os testes unitários são escritos, pode passar a escrever scripts de teste mais complexos para outros contratos inteligentes. Você também pode ler sobre como executar o teste unitário para contratos inteligentes usando o Truffle.

Próximos passos

Aqui está outro artigo tutorial.

Teste unitário com Hardhat e Celo

Como criar e testar chamadas de contrato com Celo e Hardhat

Sobre o autor

Mayowa Julius Ogungbola

Um Engenheiro de Software e redator técnico que está sempre aberto a trabalhar em novas ideias. Eu gosto de trabalhar com GitHub. Você também pode descobrir sobre o que eu tuíto e se conectar comigo no LinkedIn

Referências

Aqui está um link para o código de amostra do tutorial completo no meu GitHub, deixe um ⭐no repositório se achar útil.

Este artigo foi escrito por Mayowa Julius Ogungbola e traduzido por Diogo Jorge. O artigo original pode ser encontrado aqui.

Latest comments (0)