Vai al contenuto principale

Waffle: simulazioni dinamiche e test delle chiamate del contratto

wafflesmart contractSoliditytestsimulazione
Intermedio
Daniel Izdebski
14 novembre 2020
6 minuti letti minute read

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

La struttura del progetto sarà ora simile a:

1.
2├── contracts
3├── package.json
4└── 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;
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}
Mostra tutto
Copia

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"
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})
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:

  1. Impostiamo il contratto ERC20 simulato per restituire il saldo di 999999 token.
  2. Controlliamo se il metodo contract.check() restituisce false.

Siamo pronti a scatenare la bestia:

Un test superato

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

Esegui i test...

Due test superati

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

Tre test superati

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:

  • Test di Smart Contract con Waffle

Ultima modifica: @nhsz(opens in a new tab), 27 febbraio 2024

Questo tutorial è stato utile?