Waffle: simulações dinâmicas e testando chamadas de contrato
Do que se trata esse tutorial?
Neste tutorial, você aprenderá:
- use simulação dinâmica
- testar interações entre contratos inteligentes
Pressupostos:
- você já sabe como escrever um contrato inteligente simples em
Solidity
- você conhece o seu
JavaScript
eTypeScript
- você fez outros
tutoriais
do Waffle ou sabe alguma coisa sobre isso
Simulação dinâmica
Por que a simulação dinâmica é útil? Bem, isso permite-nos escrever testes unitários em vez de testes de integração. O que isso significa? Isso significa que não precisamos nos preocupar com as dependências dos contratos inteligentes, assim podemos testar todos eles em total isolamento. Deixe-me te mostrar como exatamente você pode fazer isso.
1. Projeto
Antes de começar, precisamos preparar um projeto simples no node.js:
mkdir dynamic-mockingcd dynamic-mockingmkdir contracts srcyarn init# or if you're using npmnpm init
Vamos começar adicionando typescript e testes de dependências - mocha & chai:
yarn add --dev @types/chai @types/mocha chai mocha ts-node typescript# or if you're using npmnpm install @types/chai @types/mocha chai mocha ts-node typescript --save-dev
Agora vamos adicionar Waffle
e ethers
:
yarn add --dev ethereum-waffle ethers# or if you're using npmnpm install ethereum-waffle ethers --save-dev
A nossa estrutura de projetos deverá ficar assim:
1.2├── contracts3├── package.json4└── test
2. Contrato inteligente
Para iniciar uma simulação dinâmica, precisamos de um contrato inteligente com dependências. Não se preocupe, nós ajudamos você!
Aqui está um simples contrato inteligente escrito na Solidity
cujo único objetivo é conferir se somos ricos. Ele usa o token ERC20 para verificar se temos tokens suficientes. Coloque em ./contracts/AmIRichAlready.sol
.
1pragma solidity ^0.6.2;23interface IERC20 {4 function balanceOf(address account) external view returns (uint256);5}67contract AmIRichAlready {8 IERC20 private tokenContract;9 uint public richness = 1000000 * 10 ** 18;1011 constructor (IERC20 _tokenContract) public {12 tokenContract = _tokenContract;13 }1415 function check() public view returns (bool) {16 uint balance = tokenContract.balanceOf(msg.sender);17 return balance > richness;18 }19}Exibir tudoCopiar
Como queremos usar simulação dinâmica, não precisamos de todo o ERC20, é por isso que estamos usando a interface IERC20 com apenas uma função.
É hora de construir este contrato! Para isso, usaremos o Waffle
. Primeiro, vamos criar um arquivo de configuração simples waffle.json
que especifica as opções de compilação.
1{2 "compilerType": "solcjs",3 "compilerVersion": "0.6.2",4 "sourceDirectory": "./contracts",5 "outputDirectory": "./build"6}Copiar
Agora estamos prontos para construir o contrato com Waffle:
npx waffle
Fácil, certo? Na pasta de compilação/
dois arquivos correspondentes ao contrato e a interface apareceu. Nós os utilizaremos mais tarde para testar.
3. Testando
Vamos criar um arquivo chamado AmIRichAlready.test.ts
para os testes reais. Em primeiro lugar, temos de lidar com as importações. Nós precisaremos deles para mais tarde:
1import { expect, use } from "chai"2import { Contract, utils, Wallet } from "ethers"3import {4 deployContract,5 deployMockContract,6 MockProvider,7 solidity,8} from "ethereum-waffle"
Exceto para dependências JS, precisamos importar nossa interface e contrato construídos:
1import IERC20 from "../build/IERC20.json"2import AmIRichAlready from "../build/AmIRichAlready.json"
Waffle usa chai
para testes. No entanto, antes de podermos usá-lo, temos que injetar os "matchers" de Waffle em si mesmo:
1use(solidity)
Precisamos implementar a função beforeEach()
que irá redefinir o estado do contrato antes de cada teste. Primeiro, vamos pensar no que precisamos lá. Para implantar um contrato, precisamos de duas coisas: uma carteira e um contrato ERC20 implementado para passá-la como um argumento para o contrato AmIRichalready
.
Em primeiro lugar, criamos uma carteira:
1const [wallet] = new MockProvider().getWallets()
Depois, precisamos de implantar um contrato do ERC20. Aqui está a parte complicada - nós temos apenas uma interface. Esta é a parte em que Waffle vem nos salvar. Waffle tem uma função mágica deployMockContract()
que cria um contrato usando apenas o abi da interface:
1const mockERC20 = await deployMockContract(wallet, IERC20.abi)
Agora com a carteira e o ERC20 implantados, podemos ir em frente e implantar o contrato AmIRichalready
(Contrato:
1const contract = await deployContract(wallet, AmIRichAlready, [2 mockERC20.address,3])
Com tudo isso, nossa função beforeEach()
está terminada. Até agora o seu arquivo AmIRichAlready.test.ts
deve se parecer com isto:
1import { expect, use } from "chai"2import { Contract, utils, Wallet } from "ethers"3import {4 deployContract,5 deployMockContract,6 MockProvider,7 solidity,8} from "ethereum-waffle"910import IERC20 from "../build/IERC20.json"11import AmIRichAlready from "../build/AmIRichAlready.json"1213use(solidity)1415describe("Am I Rich Already", () => {16 let mockERC20: Contract17 let contract: Contract18 let wallet: Wallet1920 beforeEach(async () => {21 ;[wallet] = new MockProvider().getWallets()22 mockERC20 = await deployMockContract(wallet, IERC20.abi)23 contract = await deployContract(wallet, AmIRichAlready, [mockERC20.address])24 })25})Exibir tudo
Vamos fazer o primeiro teste para o contrato AmIRichalready
. Sobre o que acha que o nosso teste deveria ser? Sim, você tem razão! Deveríamos verificar se já somos ricos :)
Um, pera um segundo. Como o nosso contrato simulado saberá quais valores retornar? Não implementamos nenhuma lógica para a função balanceOf()
. Mais uma vez, Waffle pode ajudar aqui. Nosso contrato simulado tem algumas coisas novas e bonitas agora:
1await mockERC20.mock.<nameOfMethod>.returns(<value>)2await mockERC20.mock.<nameOfMethod>.withArgs(<arguments>).returns(<value>)
Com esse conhecimento, podemos finalmente escrever nosso primeiro teste:
1it("returns false if the wallet has less than 1000000 tokens", async () => {2 await mockERC20.mock.balanceOf.returns(utils.parseEther("999999"))3 expect(await contract.check()).to.be.equal(false)4})
Vamos travar esse teste em partes:
- Definimos nosso contrato simulado no ERC20 para sempre devolver o saldo de tokens de 9999999999.
- Verifique se o método
contract.check()
retornafalse
.
Nós estamos prontos para disparar a fera:
Então o teste funciona, mas... ainda há espaço para melhorias. A função saldoOf()
sempre retornará 99999. Podemos melhorá-la especificando uma carteira para a qual a função deve retornar algo - como um contrato de verdade:
1it("returns false if the wallet has less than 1000001 tokens", async () => {2 await mockERC20.mock.balanceOf3 .withArgs(wallet.address)4 .returns(utils.parseEther("999999"))5 expect(await contract.check()).to.be.equal(false)6})
Até agora, nós testamos apenas o caso em que não estamos ricos o suficiente. Em vez disso, vamos testar o oposto:
1it("returns true if the wallet has at least 1000001 tokens", async () => {2 await mockERC20.mock.balanceOf3 .withArgs(wallet.address)4 .returns(utils.parseEther("1000001"))5 expect(await contract.check()).to.be.equal(true)6})
Você executa os testes...
E aqui está você! Nosso contrato parece funcionar como pretendido :)
Testando chamadas de contrato
Vamos resumir o que fez até agora. Nós testamos a funcionalidade do nosso contrato de AmIRichalready
e parece que ele está funcionando corretamente. Isso significa que estamos prontos, né? Não exatamente! Waffle permite-nos testar ainda mais o nosso contrato. Mas o quanto exatamente? Bem, no arsenal de Waffle há um calledOnContract()
e calledOnContractWith()
correspondentes. Eles nos permitirão verificar se nosso contrato chamado de simulação (mock, em inglês) do ERC20. Aqui está um teste básico com um desses matchers:
1it("checks if contract called balanceOf on the ERC20 token", async () => {2 await mockERC20.mock.balanceOf.returns(utils.parseEther("999999"))3 await contract.check()4 expect("balanceOf").to.be.calledOnContract(mockERC20)5})
Podemos ir ainda mais longe e melhorar este teste com o outro "matcher" que eu te falei:
1it("checks if contract called balanceOf with certain wallet on the ERC20 token", async () => {2 await mockERC20.mock.balanceOf3 .withArgs(wallet.address)4 .returns(utils.parseEther("999999"))5 await contract.check()6 expect("balanceOf").to.be.calledOnContractWith(mockERC20, [wallet.address])7})
Vamos verificar se os testes estão corretos:
Ótimo, todos os testes são verdes.
Testar chamadas de contrato com Waffle é super fácil. E aqui está a melhor parte. Esses "matchers" trabalham com contratos normais e simulados! É porque o Waffle registra e filtra chamadas EVM em vez de injetar código, como é no caso de bibliotecas de teste populares de outras tecnologias.
A Linha de Chegada
Parabéns! Agora você sabe como usar Waffle para testar chamadas de contrato e contratos simulados dinamicamente. Há características muito mais interessantes para descobrir. Recomendo mergulhar na documentação do Waffle.
A documentação do Waffle está disponível aqui(opens in a new tab).
O código fonte deste tutorial pode ser encontrado aqui(opens in a new tab).
Você pode também estar interessado em:
Última edição: @nhsz(opens in a new tab), 27 de fevereiro de 2024