Pular para o conteúdo principal

Waffle: simulações dinâmicas e testando chamadas de contrato

waffleContratos InteligentessolidityTestessimulando
Intermediário
Daniel Izdebski
14 de novembro de 2020
7 minutos de leitura minute read

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 e TypeScript
  • 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-mocking
cd dynamic-mocking
mkdir contracts src
yarn init
# or if you're using npm
npm 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 npm
npm 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 npm
npm install ethereum-waffle ethers --save-dev

A nossa estrutura de projetos deverá ficar assim:

1.
2├── contracts
3├── package.json
4└── 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;
2
3interface IERC20 {
4 function balanceOf(address account) external view returns (uint256);
5}
6
7contract AmIRichAlready {
8 IERC20 private tokenContract;
9 uint public richness = 1000000 * 10 ** 18;
10
11 constructor (IERC20 _tokenContract) public {
12 tokenContract = _tokenContract;
13 }
14
15 function check() public view returns (bool) {
16 uint balance = tokenContract.balanceOf(msg.sender);
17 return balance > richness;
18 }
19}
Exibir tudo
Copiar

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"
9
10import IERC20 from "../build/IERC20.json"
11import AmIRichAlready from "../build/AmIRichAlready.json"
12
13use(solidity)
14
15describe("Am I Rich Already", () => {
16 let mockERC20: Contract
17 let contract: Contract
18 let wallet: Wallet
19
20 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:

  1. Definimos nosso contrato simulado no ERC20 para sempre devolver o saldo de tokens de 9999999999.
  2. Verifique se o método contract.check() retorna false.

Nós estamos prontos para disparar a fera:

Um test passando

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.balanceOf
3 .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.balanceOf
3 .withArgs(wallet.address)
4 .returns(utils.parseEther("1000001"))
5 expect(await contract.check()).to.be.equal(true)
6})

Você executa os testes...

Dois testes passando

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.balanceOf
3 .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:

Três testes passando

Ó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:

  • Testando contratos inteligentes com Waffle

Última edição: @nhsz(opens in a new tab), 27 de fevereiro de 2024

Este tutorial foi útil?