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
ouyarn
: 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
ouyarn
.
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:
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.
O comando inicia um projeto de demonstração chamado <metacoin>
incluindo dois arquivos de contrato MetaCoin.sol
e 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 test
e o resultado do teste unitário deve ser exatamente como na imagem abaixo.
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.
- 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. - O Contrato
Sample.sol
terá as seguintes funcionalidades:
- a. Primeiro, o contrato é criado e as variáveis
name
eage
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;
}
- 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ávelname
.
constructor() {
owner = msg.sender;
name = "deployer";
}
- c. A próxima função
rename
aceita um valor de string como argumento e o atribui à variávelname
.
function rename(string memory _name) public {
name = _name;
}
- 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);
}
- 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"
);
_;
}
- f. A seguinte função
changeOwner
usa o modificador criado anteriormenteownerOnly
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çãochangeOwner
.
function changeOwner(address _newOwner) public ownerOnly {
owner = _newOwner;
}
- 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"
);
}
- 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);
}
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);
}
}
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.
Agora que você conhece as diferentes funções do contrato Sample.sol
e 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
});
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 chamado1_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 });
};
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");
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) => {
})
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]);
});
});
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.
- 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çãorename 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");
});
});
Agora, execute o comando npx truffle test
e um resultado bem-sucedido deve se parecer com a imagem abaixo.
- 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"
)};
});
});
Agora, execute o comando npx truffle test
e um resultado bem-sucedido deve se parecer com a imagem abaixo.
- 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"));
}
});
});
Agora, execute o comando npx truffle test
e um resultado bem-sucedido deve se parecer com a imagem abaixo.
- A próxima função
describe
testa a funçãowithdraw
no 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 palavrarevert
.
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"));
}
});
});
Por fim, execute o comando npx truffle test
e um resultado bem-sucedido deve se parecer com a imagem abaixo.
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"));
}
});
});
});
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.
Top comments (0)