Vai al contenuto principale

Aiuta ad aggiornare questa pagina

🌏

C'è una nuova versione di questa pagina, ma al momento è solo in inglese. Aiutaci a tradurre l'ultima versione.

Traduci la pagina
Visualizza in inglese

Nessun bug qui!🐛

Questa pagina non è stata tradotta. Per il momento, è stata intenzionalmente lasciata in inglese.

Waffle: simulazioni dinamiche e test delle chiamate del contratto

waffle
Smart Contract
Solidity
test
simulazione
Intermedio
✍Daniel Izdebski
��📆14 novembre 2020
⏱️6 minuti letti

Di cosa tratta questo tutorial?

In questo tutorial imparerai come:

  • usare la simulazione dinamica
  • testare le interazioni tra smart contract

Premesse:

  • Sai già come scrivere un semplice smart contract in Solidity
  • Sai già utilizzare JavaScript e TypeScript
  • Hai già 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 maniera completamente isolata. 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
# oppure, se usi 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
# oppure, se usi 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
# oppure, se usi npm
$ npm install ethereum-waffle ethers --save-dev

La struttura del progetto sarà ora simile a:

1.
2├── contracts
3├── package.json
4└── test
5

2. Smart contract

Per avviare la simulazione dinamica, occorre 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}
20
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 il contratto. A tale scopo useremo Waffle. Innanzi tutto, 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}
7
📋 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 più avanti 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"
9

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"
3

Waffle usa chai per il test. Tuttavia, prima di poterlo usare, dobbiamo inserire i matcher di Waffle in chai:

1use(solidity)
2

Dobbiamo implementare la funzione beforeEach() che ripristinerà lo stato del contratto prima di ogni test. Pensiamo intanto 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.

Per prima cosa creiamo un portafoglio:

1const [wallet] = new MockProvider().getWallets()
2

Quindi 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)
2

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])
4

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})
26
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 quali valori restituire? Non abbiamo implementato alcuna logica per la funzione balanceOf(). Anche qui Waffle ci viene in aiuto. Il nostro contratto simulato ora contiene alcuni elementi interessanti:

1await mockERC20.mock.<nameOfMethod>.returns(<value>)
2await mockERC20.mock.<nameOfMethod>.withArgs(<arguments>).returns(<value>)
3

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})
5

Dividiamo il test in due 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 il quale 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})
7

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

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})
6

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})
8

Controlliamo se i test sono corretti:

Tre test superati

Ottimo, tutti i test danno semaforo 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 immergerti nella documentazione di Waffle.

La documentazione di Waffle è disponibile qui.

Il codice sorgente di questo tutorial si può trovare qui.

Altri tutorial che potrebbero interessarti:

  • Test di smart contract con Waffle
Ultima modifica: , Invalid DateTime
Modifica la pagina

Questa pagina è stata utile?