WEB3DEV

Cover image for Um Guia Simples de Como Escrever Testes de Unidade Para Contratos Inteligentes
Panegali
Panegali

Posted on

Um Guia Simples de Como Escrever Testes de Unidade Para Contratos Inteligentes

Bons testes de unidade são críticos quando se trata de desenvolvimento de contratos inteligentes. Na verdade, como os contratos inteligentes são imutáveis ​​e geralmente responsáveis ​​pelo gerenciamento de grandes somas de dinheiro, pode-se argumentar que o desenvolvimento de bons testes de unidade para contratos inteligentes é mais importante do que os aplicativos tradicionais móveis e da Web.

Nos últimos dois meses, tivemos a sorte de trabalhar em estreita colaboração com uma empresa de serviços financeiros que está desenvolvendo uma plataforma inovadora para tokenizar títulos de maneira compatível com as regulamentações. Sua plataforma a ser lançada em breve apresentará a capacidade de fazer a transição de bilhões de dólares em títulos para a blockchain.

Em apoio ao seu lançamento, trabalhamos em estreita colaboração com sua equipe de engenharia e implementamos uma cobertura de teste abrangente e executamos nosso processo de descoberta de bugs. A partir da experiência, saímos com algumas considerações importantes, bem como algumas dicas e truques que você pode usar ao desenvolver testes de unidade para seus contratos inteligentes.

Obtenha uma compreensão saudável da lógica de negócios

A primeira coisa a fazer é certificar-se de que você tenha um entendimento completo da lógica de negócios de seu aplicativo distribuído (dapp). É útil documentar os problemas que seu dapp resolverá e como ele irá resolvê-los. Você também deverá identificar as diferentes restrições que seu dapp possui. Por exemplo, a maioria dos dapps institui alguma forma de controle de acesso baseado em função ou controle de acesso baseado em contrato. É importante documentar essas restrições e o impacto que elas terão no uso.

Além disso, você também deseja transcrever o fluxo de trabalho do seu dapp. É necessário que determinados contratos sejam ativados antes de outros? Um contrato específico deve ser pausado ou retomado para realizar uma tarefa específica? etc. Esses são os tipos de lógica de negócios e requisitos relacionados ao fluxo de trabalho que devem ser bem compreendidos antes de você mergulhar e começar a desenvolver seus testes de unidade.

Mapeie seus testes de unidade

Depois de documentar a lógica de negócios e o fluxo de trabalho do seu dapp, recomendamos que você mapeie cada teste de unidade associado à funcionalidade correspondente do seu dapp. Isso requer que você divida seu dapp, contrato por contrato e recurso por recurso e, finalmente, especifique todos os testes de unidade correspondentes associados a cada caso de uso em potencial.

Para fazer isso, agrupamos nossos testes de unidade para que sejam mapeados para as funções correspondentes nos contratos. Aqui está um exemplo de especificação para um único caso de uso associado a uma função buyTokens(_amount)no contrato de um Token:

// quando o contrato é ativado
  // quando o contrato não está pausado
     // quando o período de oferta começou
        // quando o período de oferta ainda não terminou
           // deve retornar true
Enter fullscreen mode Exit fullscreen mode

Por outro lado, aqui está outra especificação associada à função buyTokens(_amount)quando o período de oferta terminar:

// quando o contrato é ativado
  // quando o contrato não está pausado
     // quando o período de oferta começou
        // quando o período de oferta terminar
            // deve reverter
Enter fullscreen mode Exit fullscreen mode

Mapear todos os seus testes de unidade dessa forma é uma boa maneira de facilitar as conversas com as partes interessadas e o restante da equipe de engenharia. Isso ajuda a garantir que todos estejam na mesma página quando se trata dos requisitos do seu dapp. Além disso, ajuda a orientá-lo sobre como arquitetar e construir seu dapp de maneira segura e confiável.

Aborde os modificadores primeiro, depois trabalhe com o suas declarações require e if sequencialmente

Depois de mapear seus testes de unidade, recomendamos que você se concentre primeiro em abordar os casos de uso associados aos seus modificadores de função. Em seguida, você pode percorrer a função sequencialmente e desenvolver testes de unidade para abordar todos os casos de uso associados a cada instrução require e if.

Por exemplo, para uma função como a seguinte, gostaríamos primeiro de abordar os casos de uso associados aos modificadores atStage(Stages.AuctionStarted)e validBid() antes de abordar as declarações if e else que se seguem.

/// @dev O primeiro a fornecer um lance no CurrentAskingPrice recebe o NFT do beneficiário 
/// @dev Se um licitante fizer um lance maior no NFT, ele vencerá o leilão e sua função excedente será retornada
processBid()
public
payable
atStage(Stages.AuctionStarted) // Comece aqui primeiro
validBid() // Aborde esses casos de uso em segundo lugar
returns (uint) // Em seguida, enderece as instruções `if`, `else` e `require` que seguem
{
 bid = msg.value;
 bidder = msg.sender;

 getCurrentAskingPrice();    
 if (bid > currentAskingPrice)
 {     
   overage = bid.sub(currentAskingPrice); 
   bidder.transfer(overage);     
   bid = currentAskingPrice;     
   stage = Stages.AuctionEnded;       
   payBeneficiary();     
   sendToBidder();   
 }
 else if (bid == currentAskingPrice)
 {
   stage = Stages.AuctionEnded;
   payBeneficiary();     
   sendToBidder();   
 }
 else
 {
   revert("Bid é menor que currentAskingPrice");   
 } 
}
Enter fullscreen mode Exit fullscreen mode

Para testar quando seus contratos devem ser revertidos ao original (revert), descobrimos que o auxiliar assertRevert do OpenZeppelin é extremamente útil. Você pode usar assim:

await assertRevert(tokenInstance.buy({from: investor, value: 500}));
Enter fullscreen mode Exit fullscreen mode

Sobrecarga de funções e necessidade de chamadas de baixo nível

À medida que você continua a desenvolver seus testes de unidade, é provável que encontre deficiências associadas às ferramentas de desenvolvedor que está usando. Isso ocorre porque o espaço de desenvolvimento de contratos inteligentes ainda é muito novo e, consequentemente, muitas das ferramentas de desenvolvedor ainda são imaturas.

Por exemplo, o framework Truffle – que é uma excelente ferramenta e talvez o framework mais popular no espaço Ethereum hoje – não suporta sobrecarga de função. Ou seja, se você precisar testar duas funções com o mesmo nome, mas aridades diferentes, precisará usar chamadas de baixo nível para testar a segunda função na ABI do contrato. Caso contrário, você receberá um Error: Invalid number of arguments to Solidity function. Vamos dar uma olhada em um exemplo rápido.

Se você tiver duas funções buyTokens em um contrato, uma que não aceita argumentos e está listada primeiro na ABI e outra que aceita um argumento (_amount) e está listada em segundo lugar na ABI, você precisará usar uma chamada de baixo nível para testar a função buyTokens(_amount) usando encodeCall como visto abaixo.

data = encodeCall("buyTokens",['uint'],[100]); // 100 é o valor do argumento `_amount`
contractInstance.sendTransaction({data, from: investor, value: 500})
Enter fullscreen mode Exit fullscreen mode

A comunidade Truffle está ciente desse problema e ele será resolvido em uma próxima versão. No entanto, cenários peculiares como esse não são atípicos quando você está desenvolvendo contratos inteligentes e seus testes de unidade correspondentes. Você terá que ser astuto (mas extremamente cuidadoso) com as soluções que invoca de vez em quando.

Como testar funções internal

Algumas funções no Solidity podem ter visibilidades diferentes (ou seja, public, private, internal e external), vale a pena notar que desenvolver testes de unidade para funções internal é um pouco diferente de desenvolvê-las para funções public. Isso ocorre porque as funções internal não são listadas na ABI de um contrato inteligente após sua compilação. Portanto, para testar as funções internal, você tem duas opções:

  1. Você pode criar outro contrato que herde o contrato que está testando para testar uma função internal
  2. Ou você pode testar a lógica de uma função internal dentro das outras funções no contrato que chamam a função internal

Qualquer uma das abordagens fará o trabalho, embora alguns possam argumentar que testar a lógica de uma função internal de dentro das outras funções do contrato é mais intuitivo.

Dicas e truques práticos do Ganache

Como mencionei anteriormente, usamos principalmente o framework Truffle para criar dapps para nossos clientes. Truffle usa uma ferramenta chamada Ganache que permite que você acione rapidamente sua própria blockchain Ethereum pessoal, que você pode usar para executar testes, executar comandos e inspecionar o estado enquanto controla como a cadeia opera. É super útil.

O que é realmente bom sobre o Ganache é como ele pode ser facilmente personalizado para satisfazer as necessidades exclusivas do seu dapp. Por exemplo, desenvolvemos testes de unidade para dapps que exigem que testemos casos de uso para dezenas e dezenas de usuários. Com um comando simples, o Ganache facilita o teste com quantas contas forem necessárias:

ganache-cli -a 40 // Onde a bandeira `a' denota o início do Ganache com 40 contas de teste
Enter fullscreen mode Exit fullscreen mode

Além disso, podemos definir os saldos dessas contas para começar com o máximo de ETH necessário para nossos testes. Por padrão, o Truffle define os saldos em 100 ETH. Podemos facilmente aumentar ou diminuir esse valor, dependendo de nossos próprios requisitos exclusivos:

ganache -e 10000` // Onde a bandeira "e" denota com quanto ETH cada conta deve começar por padrão
Enter fullscreen mode Exit fullscreen mode

Ganache é uma ferramenta extremamente útil quando se trata de desenvolver contratos inteligentes para Ethereum. Ele ajuda a simplificar o desenvolvimento, reduzir riscos e melhorar a confiabilidade do dapp.

Amarrando tudo junto

O teste de unidade no espaço de contrato inteligente é fundamentalmente semelhante ao teste de unidade na web e no mundo dos aplicativos móveis. A maior diferença é que os contratos inteligentes hoje são imutáveis ​​e geralmente não oferecem maneiras simples de fazê-los ao longo do tempo [1]. Portanto, desenvolver bons testes de unidade é fundamental, dada a imutabilidade dos contratos inteligentes e o fato de que eles geralmente são responsáveis ​​pelo gerenciamento de grandes somas de dinheiro. A única maneira de ter certeza de que seus contratos inteligentes são seguros e confiáveis ​​é testá-los meticulosamente.

Para a adoção em massa de contratos inteligentes, é fundamental que nossa comunidade trabalhe em conjunto para avançar no espaço e tornar mais fácil para os desenvolvedores escrever aplicativos complexos em blockchains públicas.

Esperamos que essas lições aprendidas com nossas experiências no desenvolvimento de testes de unidade sejam úteis e apoiem seus esforços para criar contratos inteligentes seguros e confiáveis. Não hesite em entrar em contato se tiver outras perguntas ou ideias que gostaria de discutir com nossa equipe — [email protected].

Nos próximos dias, publicaremos um artigo de acompanhamento para este, O que aprendi ao testar contratos inteligentes complexos.

[1] A equipe do Zeppelin lançou recentemente o ZeppelinOS, que fornece um conjunto on-chain de bibliotecas-padrão atualizáveis ​​e torna possível atualizar seus contratos inteligentes usando padrões de proxy.

Para se manter atualizado com nosso trabalho de desenvolvimento de contratos inteligentes, siga-nos no Medium e no Twitter.


Artigo escrito por Doug Crescenzi e traduzido por Marcelo Panegali.

Top comments (0)