Foto de Shubham Dhage no Unsplash
O teste é uma parte importante de qualquer projeto de desenvolvimento de software, mas provou ser especialmente crucial para o sucesso dos projetos Web3. Onde você pode ter registros defeituosos ou pagamentos com falha em aplicativos Web2, os bugs introduzidos em aplicativos Web3 podem permitir que os usuários diminuam o valor dos tokens cunhando tokens em excesso, alterem as regras de como uma DAO opera ou até mesmo congelem permanentemente fundos vinculados a um contrato inteligente.
Em projetos Web2, os bugs podem ser corrigidos com ramificações simples de correção que são implantadas antes que outros dados sejam corrompidos ou que as vendas sejam perdidas. Este não é o caso com a Web3. Bugs em um aplicativo Web3 são permanentes, pois qualquer contrato inteligente implantado na blockchain é imutável. Você pode encontrar uma correção, mas uma vez que seu contrato esteja ativo, é impossível implementar a correção.
Como devemos escrever um código invulnerável a ataques? A ideia de segurança perfeita, mesmo na Web3, é uma aspiração impossível. No entanto, com testes adequados e cobertura total, podemos diminuir as superfícies de ataque para agentes mal-intencionados.
Os projetos do Clarity criados com o Clarinet vêm com um framework de teste TypeScript integrado. Usando esse framework, podemos implantar nossos contratos inteligentes em cadeias de simulação e executar nossas várias funções nele. Isso nos permite simular funções públicas e de somente leitura e verificar como elas acessam e alteram o estado da cadeia.
Há duas linhas comuns a todos os arquivos de teste do Clarity TypeScript:
Figura 1. Importações do módulo TypeScript principal para teste do Clarinet.
Neste artigo, gostaria de analisar mais detalhadamente o que esses objetos de importação estão incluindo em suas propriedades e como eles são usados para escrever um código mais limpo e livre de erros.
Clarinet
A primeira classe importada do arquivo index.ts é uma das mais importantes para nós. Esta classe é a Clarinet, que é a chave para todas as nossas configurações de teste e para a capacidade de criar e testar na blockchain. Com o Clarinet, podemos chamar Clarinet.test(), que é nossa função de encapsulamento para todos os testes de unidade que desejamos realizar em nossos contratos inteligentes.
Vamos detalhar passo a passo o objeto de importação do Clarinet e examinar os diferentes itens que ele está abordando em nossa configuração de teste de unidade. Primeiro, precisamos instanciar nossa instância do Clarinet:
Figura 2. Chamada de método da classe de teste do objeto Clarinet.
Para começar, declaramos o método da classe de teste, que é nosso principal e único método na classe Clarinet. Este método receberá um hash como objeto com um tipo UnitTestOptions incluindo nosso nome, que é a descrição do teste de unidade específico, os valores somente opcionais e os opcionais ignorados e, finalmente, nossa função para executar o teste de unidade. O restante desta seção é um preâmbulo para nossa configuração da cadeia de teste.
Depois que isso estiver concluído, passamos para a configuração da nossa cadeia e das contas de teste:
Figura 3. Lógica do método Chain.test para criar cadeia e contas com um valor beforeContractsDeployment.
Se tivermos um valor para beforeContractsDeployment, faremos três coisas. Primeiro, vamos criar uma nova instância do nosso objeto Chain usando o session_id atual. Em seguida, criaremos uma instância Map vazia com chaves de string e valores da instância Account. Por fim, vamos semear esse mapa de contas vazio usando as contas retornadas de nossa opção de resultado criada na lógica de preâmbulo realizada na figura anterior.
Em seguida, usamos esses dois objetos semeados para chamar nosso método beforeContractsDeployment e, em seguida, definimos nosso sessionId como o sessionId de nossa nova instância da cadeia de teste.
Nossa seção final do código da classe Chain trata da criação da cadeia e das contas, caso ainda não tenham sido concluídas:
Figura 4. Criando cadeia e contas sem o valor beforeContractsDeployment e chamando nossa função.
Ela também cria um mapa para nossos diferentes contratos e chama o options.fn usando nossa nova cadeia de teste, contas de teste e a coleção de contratos relacionados ao nosso conjunto de testes. Nada pode ser executado sem essa classe, portanto, espero que este aprofundamento ajude você a entender a configuração de teste dos conjuntos de testes do Clarity TypeScript.
Tx
O objeto de classe Tx é usado para chamar 3 métodos de classe diferentes. Esses métodos são transferSTX, contractCall e deployContract. Todos esses 3 métodos realizam transações na cadeia. Antes de entrarmos nesses métodos, vamos primeiro dar uma olhada na instanciação do Tx:
Figura 5. Método construtor e interface para a classe Tx.
Embora não tenhamos instanciado explicitamente os objetos Tx, todos os 3 métodos listados acima instanciam objetos Tx em suas respectivas chamadas de método. Cada método tem um número: 1, 2 ou 3 que corresponde ao método que está chamando. Com base nas chamadas de método, também temos acesso a qualquer uma das três interfaces opcionais listadas abaixo:
Figura 6. Interfaces para o objeto de retorno de transferência, implantação ou chamada na cadeia.
Cada uma dessas interfaces é usada para formatar e impor os tipos de dados retornados para as chamadas de método. A própria classe Tx se preocupa principalmente em definir e rastrear o tipo de método de transação que está chamando e o endereço principal que está chamando para que a transação ocorra.
O primeiro método, transferSTX aceita uma quantia, um destinatário e um remetente:
Figura 7. Método para transferência de STX entre dois endereços.
Ele cria uma nova instância de Tx usando o tipo 1 e o endereço principal do remetente. Em seguida, ele usa a interface TxTransfer para retornar um objeto com o destinatário e o valor que deve mover a quantidade de moedas do remetente para o destinatário.
Nosso segundo método Tx é o contractCall. Ele cria novamente uma instância de Tx usando o remetente principal e o tipo 2. Depois disso, ele usa a interface TxContractCall para criar um objeto incluindo o contrato inteligente, o método que está sendo chamado e nossos argumentos para o método:
Figura 8. Método para fazer chamadas para um contrato já na blockchain.
Nosso método final para a classe Tx é o deployContract. Este método aceita um nome, um código e o remetente, e retorna uma instância da interface TxDeployContract que:
Figura 9. Método da classe Tx para implantação de um contrato na cadeia.
Esses três métodos de classe lidam com as principais transações que gostaríamos de testar, incluindo a implantação de um contrato na blockchain, a transferência de tokens entre os princípios padrão e, finalmente, a realização de chamadas de método para métodos de contrato inteligente existentes.
Chain
Para Chain em index.ts, existem dois objetos diferentes exportados do arquivo. O primeiro objeto é uma interface Chain que possui um único atributo, sessionId:
Figura 10. Interface para instâncias do objeto Chain.
O objeto mais interessante vem depois com o objeto da classe Chain. Semelhante à interface, precisamos de um sessionId para instanciar a classe Chain. Ele usa o construtor para definir o sessionId e, em seguida, tem um valor blockHeight padrão definido como 1:
Figura 11. Classe Chain com método construtor e atributos de classe.
Uma vez que temos nossa instância do objeto Chain, podemos usar qualquer um de seus 5 métodos de instância diferentes para adicionar ou ler a partir da cadeia. Os métodos de primeira instância são mineBlock:
Figura 12. O método de instância mineBlock do objeto Chain.
Para poder testar funções públicas, precisamos adicioná-las como transações dentro de um bloco à blockchain. Para fazer isso, precisaremos alterar o estado da blockchain, o que requer a adição de um novo bloco a ela. O método mineBlock aceita uma matriz de transações ou objetos Tx.
Analisamos o JSON retornado pelo método mine_block e colocamos os valores em uma variável de resultado. A partir desse resultado, podemos determinar e atribuir o novo valor de altura do bloco (blockHeight) e criar um objeto de bloco usando a nova altura e os recibos ou resultados da execução de cada uma das transações em nossa matriz de transações.
O próximo método, mineEmptyBlock, é semelhante, mas em vez de aceitar uma matriz de transações a serem executadas antes de adicionar os blocos à cadeia, especificamos um número de blocos vazios para adicionar à blockchain:
Figura 13. Método de instância mineEmptyBlock da cadeia para adicionar blocos em branco à cadeia.
Nosso terceiro método de instância para a classe Chain é mineEmptyBlockUntil. Isso é muito útil quando estamos testando o estado da cadeia que ocorre em um futuro distante. O método aceita um argumento, targetBlockHeight, que nos permite especificar em qual bloco queremos chegar e então chama mineEmptyBlock até que a altura da blockchain seja igual à altura solicitada:
Figura 14. Método mineEmptyBlockUntil da cadeia para adicionar blocos vazios à cadeia até uma certa altura.
As funções somente leitura não alteram o estado da blockchain nem adicionam mais blocos a ela. Elas precisam acessar o estado da cadeia e para isso temos o método de instância do objeto Chain, callReadOnlyFn. Assim como chamar uma função pública, callReadOnlyFn requer um contrato para testar, um método para chamar, uma matriz de argumentos para o método fornecido e um endereço em formato de string para determinar quem está chamando esse método:
Figura 15. Método da cadeia para acessar o estado na instância da cadeia.
Chamamos call_read_only_fn usando esses argumentos para obter um objeto result com um session_id, resultado e eventos. Esses itens, que são retornados para nós como readOnlyFn, são bons para determinar os eventos que ocorreram e qualquer estado da cadeia que possamos ter tentado acessar com nosso método somente leitura.
Nosso método de instância final do Chain é o getAssetsMaps. Este é um comando de CLI bem útil que você pode executar com o console do clarinet como ::get_assets_maps. Ao usar isso no terminal, é exibida uma tabela de todos os endereços e seus saldos STX atuais. Este método faz uma chamada para get_assets_maps usando o sessionId da cadeia atual e retorna um objeto com o sessionId assim como um objeto result.assets:
Figura 16. Retorna um objeto incluindo os ativos ou uma lista de todos os endereços e seus saldos STX.
Account
O objeto Account é uma interface simples do TypeScript com 5 campos:
Figura 17. Interface para objeto Account.
Esses cinco campos são tudo o que precisamos para preencher o objeto principal. Aqui temos o endereço ou o valor da string para chamar para o envio e recebimento de fundos. Então temos o saldo, ou a quantidade de STX a que a conta tem direito. Em seguida, há um nome que é usado para identificar e chamar mais facilmente a conta durante os testes. Por fim, temos um mnemônico, usado para proteger a conta e um valor de string de derivação.
types (tipos)
A importação de tipos não é uma classe, mas um espaço de nomes TypeScript. Os espaços de nomes são uma maneira de realizar duas coisas importantes ao escrever código limpo e claro. Primeiro, eles agrupam códigos semelhantes em unidades. Essas unidades são colocadas em um encapsulador dentro do espaço de nomes que permite a reutilização de nomes entre espaços de nomes. Por que isso é importante? Com espaço de nomes, podemos manter nomes semelhantes e significativos para funções diferentes sem colisões de nomes.
Este espaço de nomes de tipos é extremamente longo e repetitivo, então darei uma versão condensada do que ele realiza. O Clarity tem vários tipos diferentes que não são nativos do TypeScript ou do JavaScript. Para acomodar os diferentes tipos, esse espaço de nomes de tipos serve como uma coleção de funções que podem ou não ser aceitas e, em seguida, recebem e produzem um tipo e valor que correspondem ao que o Clarity usa.
Um exemplo desses métodos são os tipos de resposta err e ok:
Figura 18. Dois tipos de métodos de espaço de nomes para formatação de respostas do Clarity.
Quando chamamos types.ok ou types.err, também passamos uma string para os valores. Esses métodos aceitam essa string e retornam as respostas valor
“ok” ou valor
“err (erro)” que esperamos para respostas do Clarity. Se você entender como esses métodos de resposta funcionam, deverá entender a maioria dos métodos no espaço de nomes de tipos.
Conclusão
Se uma boa cobertura de teste é crucial para bons projetos Web3, então conhecer suas ferramentas de teste é crucial para escrever excelentes testes de cobertura completa. Espero que depois de ler esse texto, você tenha uma melhor compreensão de quais são as principais importações do método Clarity em seus arquivos de teste do Clarinet TypeScript!
Notas
https://deno.land/x/[email protected]/index.ts
Artigo original publicado por Daniel Pericich. Tradução por Paulinho Giovannini.
Latest comments (0)