Waffle: simulazioni dinamiche e test delle chiamate del contratto
A cosa serve questo tutorial?
In questo tutorial imparerai come:
- usare la simulazione dinamica
- testare interazioni tra Smart Contract
Premesse:
- sai già come scrivere un semplice Smart Contract in
Solidity
- sai utilizzare
JavaScript
eTypeScript
- hai seguito altri tutorial di
Waffle<
o ne sai già qualcosa
Simulazione dinamica
Perché la simulazione dinamica è utile? Ci consente di scrivere unit test anziché test di integrazione. Cosa significa? Che non dobbiamo preoccuparci delle dipendenze tra gli Smart Contract, dunque possiamo testarli tutti in completo isolamento. Vediamo come.
1. Progetto
Prima di iniziare dobbiamo preparare un semplice progetto node.js:
mkdir dynamic-mockingcd dynamic-mockingmkdir contracts srcyarn init# or if you're using npmnpm init
Iniziamo aggiungendo dipendenze typescript e di test: mocha e 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
Ora aggiungiamo Waffle
e ethers
:
yarn add --dev ethereum-waffle ethers# or if you're using npmnpm install ethereum-waffle ethers --save-dev
La struttura del progetto sarà ora simile a:
1.2├── contracts3├── package.json4└── test
2. Smart Contract
Per avviare la simulazione dinamica, serve uno Smart Contract con dipendenze. Nessun problema.
Ecco un semplice Smart Contract scritto in Solidity
con il solo scopo di controllare se siamo ricchi. Usa il token ERC20 per verificare se abbiamo abbastanza token. Inseriscilo in ./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}Mostra tuttoCopia
Poiché vogliamo usare la simulazione dinamica, non ci serve tutto ERC20, quindi usiamo l'interfaccia IERC20 con una sola funzione.
È ora di creare questo contratto. Per questo useremo Waffle
. Prima, creeremo un semplice file di configurazione waffle.json
che specifichi le opzioni di compilazione.
1{2 "compilerType": "solcjs",3 "compilerVersion": "0.6.2",4 "sourceDirectory": "./contracts",5 "outputDirectory": "./build"6}Copia
Ora siamo pronti a creare il contratto con Waffle:
npx waffle
Facile, vero? Nella cartella build/
sono comparsi due file corrispondenti al contratto e all'interfaccia. Li useremo dopo per i test.
3. Test
Creiamo un file chiamato AmIRichAlready.test.ts
per il test reale. Prima di tutto, dobbiamo gestire le importazioni. Ci serviranno dopo:
1import { expect, use } from "chai"2import { Contract, utils, Wallet } from "ethers"3import {4 deployContract,5 deployMockContract,6 MockProvider,7 solidity,8} from "ethereum-waffle"
A parte le dipendenze JS, dobbiamo importare il contratto e l'interfaccia creati:
1import IERC20 from "../build/IERC20.json"2import AmIRichAlready from "../build/AmIRichAlready.json"
Waffle usa chai
per il test. Tuttavia, prima di poterlo usare, dobbiamo inserire i matcher di Waffle in chai:
1use(solidity)
Dobbiamo implementare la funzione beforeEach()
che ripristinerà lo stato del contratto prima di ogni test. Prima pensiamo a cosa ci serve. Per distribuire un contratto servono due cose: un portafoglio e un contratto ERC20 distribuito da passare come argomento per il contratto AmIRichAlready
.
Prima creiamo un portafoglio:
1const [wallet] = new MockProvider().getWallets()
Poi dobbiamo distribuire un contratto ERC20. Ecco la parte complicata: abbiamo solo un'interfaccia. Questa è la parte in cui Waffle ci viene in aiuto. Waffle ha la funzione magica deployMockContract()
che crea un contratto usando solo l'abi dell'interfaccia:
1const mockERC20 = await deployMockContract(wallet, IERC20.abi)
Ora con il portafoglio e l'ERC20 distribuito, possiamo continuare e distribuire il contratto AmIRichAlready
:
1const contract = await deployContract(wallet, AmIRichAlready, [2 mockERC20.address,3])
A questo punto, la funzione beforeEach()
è finita. Il file AmIRichAlready.test.ts
avrà il seguente aspetto:
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})Mostra tutto
Scriviamo il primo test per il contratto AmIRichAlready
. Cosa pensi dovrebbe fare il test? Esatto! Dobbiamo controllare se siamo già ricchi :)
Ma aspetta un attimo. Come farà il nostro contratto simulato a sapere che valori restituire? Non abbiamo implementato alcuna logica per la funzione balanceOf()
. Di nuovo, Waffle ci viene in aiuto. Il nostro contratto simulato ha contenuto interessante:
1await mockERC20.mock.<nameOfMethod>.returns(<value>)2await mockERC20.mock.<nameOfMethod>.withArgs(<arguments>).returns(<value>)
Con queste informazioni possiamo finalmente scrivere il primo test:
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})
Suddividiamo questo test in più parti:
- Impostiamo il contratto ERC20 simulato per restituire il saldo di 999999 token.
- Controlliamo se il metodo
contract.check()
restituiscefalse
.
Siamo pronti a scatenare la bestia:
Quindi il test funziona, ma... si può ancora migliorare. La funzione balanceOf()
restituirà sempre 99999. Possiamo migliorarla specificando un portafoglio per cui la funzione deve restituire qualcosa, proprio come un vero contratto:
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})
Finora, abbiamo testato solo il caso in cui non siamo abbastanza ricchi. Testiamo invece l'opposto:
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})
Esegui i test...
...ed ecco qui! Il nostro contratto sembra funzionare come previsto :)
Test delle chiamate del contratto
Ricapitoliamo cosa abbiamo fatto finora. Abbiamo testato la funzionalità del contratto AmIRichAlready
che sembra funzionare correttamente. Quindi abbiamo finito, giusto? Non proprio. Waffle ci consente di testare il nostro contratto ancora più a fondo. Ma come esattamente? Beh, nell'arsenale di Waffle ci sono i matcher calledOnContract()
e calledOnContractWith()
. Ci consentiranno di verificare se il contratto ha chiamato il contratto simulato ERC20. Ecco un test di base con uno di questi matcher:
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})
Possiamo andare persino oltre e migliorare questo test con l'altro matcher che ho indicato:
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})
Controlliamo se i test sono corretti:
Ottimo, tutti i test danno luce verde.
Testare le chiamate di contratto con Waffle è facilissimo. Ma ecco la parte migliore. Questi matcher funzionano sia con contratti normali che simulati! Questo perché Waffle registra e filtra le chiamate all'EVM piuttosto che inserire il codice, come invece fanno librerie di testing popolari di altre tecnologie.
Il traguardo
Congratulazioni! Ora sai come usare Waffle per testare le chiamate di contratto e i contratti simulati dinamicamente. Ci sono funzionalità ben più interessanti da scoprire. Ti consiglio di tuffarti nella documentazione di Waffle.
La documentazione di Waffle è disponibile qui(opens in a new tab).
Il codice sorgente di questo tutorial si può trovare qui(opens in a new tab).
Altri tutorial che potrebbero interessarti:
Ultima modifica: @nhsz(opens in a new tab), 27 febbraio 2024