Waffle: Simularea dinamică și testarea apelurilor de contracte
Despre ce este acest tutorial?
În acest tutorial vei învăța cum:
- să utilizezi simularea dinamică
- să testezi interacțiunile dintre contractele inteligente
Ipoteze:
- știi deja cum să scrii un simplu contract inteligent în
Solidity
- te descurci în
JavaScript
șiTypeScript
- ai terminat alte tutoriale
Waffle
sau știi un lucru sau două despre ele
Simulare dinamică
De ce este utilă simularea dinamică? Ei bine, ne permite să scriem teste unitare în loc de teste de integrare. Ce înseamnă aceasta? Înseamnă că nu trebuie să ne facem griji cu privire la dependențele contractelor inteligente, astfel că le putem testa pe toate în mod izolat. Permite-mi să-ți arăt exact cum o poți face.
1. Proiect
Înainte de a începe, trebuie să pregătim un proiect simplu node.js:
$ mkdir dynamic-mocking$ cd dynamic-mocking$ mkdir contracts src$ yarn init# sau dacă utilizezi npm$ npm init
Să începem cu adăugarea de typescript și dependențe de testare - mocha & chai:
$ yarn add --dev @types/chai @types/mocha chai mocha ts-node typescript# sau dacă utilizezi npm$ npm install @types/chai @types/mocha chai mocha ts-node typescript --save-dev
Acum să adăugăm Waffle
și eter
:
$ yarn add --dev ethereum-waffle ethers# sau dacă utilizezi npm$ npm install ethereum-waffle ethers --save-dev
Structura proiectului tău ar trebui să arate astfel:
1.2├── contracts3├── package.json4└── test5
2. Contracte inteligente
Pentru a începe simularea dinamică, avem nevoie de un contract inteligent cu dependențe. Nu-ți face griji, ne-am ocupat noi!
Iată un contract inteligent simplu scris în Solidity
al cărui unic scop este să verificăm dacă suntem bogați. Acesta folosește tokenul ERC20 pentru a verifica dacă avem suficiente tokenuri. Să spunem că avem ./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}20Afișează totCopiere
Deoarece vrem să folosim simularea dinamică, nu avem nevoie de întregul ERC20, de aceea folosim interfața IERC20 cu o singură funcție.
Este timpul să construim acest contract! Pentru aceasta vom folosi Waffle
. În primul rând, vom crea un fișier simplu de configurare Waffle.json
care specifică opțiunile de compilare.
1{2 "compilerType": "solcjs",3 "compilerVersion": "0.6.2",4 "sourceDirectory": "./contracts",5 "outputDirectory": "./build"6}7Copiere
Acum suntem gata să construim contractul cu Waffle:
$ npx waffle
Simplu, nu? În directorul build/
au apărut două fișiere corespunzătoare contractului și interfeței. Le vom folosi mai târziu pentru testare.
3. Testare
Să creăm un fișier numit AmIRichAlready.test.ts
pentru actualul test. În primul rând, trebuie să ne ocupăm de importuri. Vom avea nevoie de ele pentru mai târziu:
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
Cu excepția dependențelor JS, trebuie să importăm contractul și interfața construită:
1import IERC20 from "../build/IERC20.json"2import AmIRichAlready from "../build/AmIRichAlready.json"3
Waffle folosește chai
pentru testare. Cu toate acestea, înainte de a-l putea folosi, trebuie să injectăm validatori-matcher Waffle chiar în chai:
1use(solidity)2
Trebuie să implementăm funcția beforeEach()
care va reseta starea contractului înainte de fiecare test. Să ne gândim mai întâi la ce avem nevoie acolo. Pentru a implementa un contract avem nevoie de două lucruri: (1) un portofel și (2) un contract ERC20 implementat pentru a-l transmite ca argument pentru contractul AmIRichAlready
.
În primul rând creăm un portofel:
1const [wallet] = new MockProvider().getWallets()2
Apoi, trebuie să implementăm un contract ERC20. Iată partea dificilă - avem doar o interfață. Aceasta este partea în care Waffle ne salvează. Waffle are o funcție magică deployMockContract ()
care creează un contract folosind doar abi-ul interfeței:
1const mockERC20 = await deployMockContract(wallet, IERC20.abi)2
Acum, având portofelul și ERC20 implementate, putem continua cu implementarea contractului AmIRichAlready
:
1const contract = await deployContract(wallet, AmIRichAlready, [2 mockERC20.address,3])4
Cu toate acestea, funcția noastră beforeEach()
este terminată. Până acum fișierul tău AmIRichAlready.test.ts
ar trebui să arate astfel:
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})26Afișează tot
Să scriem primul test la contractul AmIRichAlready
. Despre ce crezi că ar trebui să fie testul nostru? Da, ai dreptate! Ar trebui să verificăm dacă suntem deja bogați :)
Dar așteaptă o secundă. Cum va ști contractul nostru simulat ce valori să returneze? Nu am implementat nicio logică pentru funcția balanceOf()
. Din nou, Waffle poate fi de ajutor aici. Contractul nostru simulat are câteva lucruri noi sofisticate acum:
1await mockERC20.mock.<nameOfMethod>.returns(<value>)2await mockERC20.mock.<nameOfMethod>.withArgs(<arguments>).returns(<value>)3
Cu aceste cunoștințe putem, în sfârșit, scrie primul nostru test:
1it("returnează fals dacă portofelul are mai puțin de 1000000 tokenuri", async () => {2 await mockERC20.mock.balanceOf.returns(utils.parseEther("999999"))3 expect(await contract.check()).to.be.equal(false)4})5
Să descompunem acest test în părți:
- Am stabilit contractul nostru fals ERC20 pentru a returna întotdeauna soldul de 999999 de tokenuri.
- Am verificat dacă metoda
contract.check()
returneazăfalse
.
Suntem gata să dăm drumul fiarei:
Deci, testul funcționează, dar... mai este loc pentru îmbunătățiri. Funcția balanceOf()
va returna întotdeauna 99999. Putem îmbunătăți acest lucru prin specificarea unui portofel pentru care funcția ar trebui să returneze ceva - la del ca un contract real:
1it("returnează fals dacă portofelul are mai puțin de 1000001 tokenuri", async () => {2 await mockERC20.mock.balanceOf3 .withArgs(wallet.address)4 .returns(utils.parseEther("999999"))5 expect(await contract.check()).to.be.equal(false)6})7
Până acum, am testat doar cazul în care nu suntem suficient de bogați. Să testăm opusul în schimb:
1it("returnează true dacă portofelul are cel puțin 1000001 tokenuri", async () => {2 await mockERC20.mock.balanceOf3 .withArgs(wallet.address)4 .returns(utils.parseEther("1000001"))5 expect(await contract.check()).to.be.equal(true)6})7
Rulează testele...
...și iată! Contractul nostru pare să funcționeze conform scopului :)
Testarea apelurilor de contracte
Să rezumăm ce am făcut până acum. Am testat funcționalitatea contractului AmIRichAlready
și se pare că funcționează corect. Asta înseamnă că am terminat, nu? Nu chiar! Waffle ne permite să testăm și mai mult contractul. Dar cum anume? Ei bine, în arsenalul Waffle există doi validatori-matcher calledOnContract()
și calledOnContractWith()
. Ei ne vor permite să verificăm dacă nu cumva contractul nostru a apelat contractul simulat ERC20. Iată un test de bază cu unul dintre acești validatori-matcher:
1it("contractul nostru este verificat dacă a apelat balanceOf pe tokenul ERC20", async () => {2 await mockERC20.mock.balanceOf.returns(utils.parseEther("999999"))3 await contract.check()4 expect("balanceOf").to.be.calledOnContract(mockERC20)5})6
Putem merge chiar mai departe și putem îmbunătăți acest test cu celălalt validator-matcher de care am povestit:
1it("contractul nostru este verificat dacă a apelat balanceOf cu un anumit portofel pe tokenul ERC20", 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})8
Să verificăm dacă testele sunt corecte:
Super, toate testele sunt verzi.
Testarea apelurilor de contracte cu Waffle este foarte ușoară. Și aici e partea cea mai bună. Acești validatori-matcher funcționează cu contracte normale sau simulate! Aceasta deoarece Waffle înregistrează și filtrează apelurile EVM în loc să injecteze cod, ca în cazul bibliotecilor populare de testare pentru alte tehnologii.
Linia de sosire
Felicitări! Acum știi cum să folosești Waffle pentru a testa în mod dinamic apelurile de contracte și contractele simulate. Există mult mai multe caracteristici interesante de descoperit. Îți recomandăm să studiezi în detaliu documentația Waffle.
Documentația Waffle este disponibilă aici.
Codul sursă pentru acest tutorial poate fi găsit aici.
Tutoriale care te pot interesa, de asemenea: