Waffle: Dynamiczne tworzenie atrap i testowanie wywołań kontraktów
O czym jest ten samouczek?
Z tego samouczka dowiesz się, jak:
- uużywać dynamicznego tworzenia atrap
- testować interakcje między inteligentnymi kontraktami
Założenia:
- wiesz już, jak napisać prosty inteligentny kontrakt w
Solidity
- znasz się na
JavaScript
iTypeScript
- zapoznałeś się z innymi samouczkami
Waffle
lub wiesz coś na ten temat
Dynamiczne tworzenie atrap
Dlaczego dynamiczne tworzenie atrap jest przydatne? No cóż, pozwala nam pisać testy jednostkowe zamiast testów integracyjnych. Co to oznacza? Oznacza to, że nie musimy martwić się zależnościami inteligentnych kontraktów, dlatego możemy je przetestować w całkowicie izolacji. Pozwolę sobie pokazać, jak dokładnie możesz to zrobić.
1. Projekt
Zanim zaczniemy musimy przygotować prosty projekt node.js:
$ mkdir dynamic-mocking$ cd dynamic-mocking$ mkdir contracts src$ yarn init# or if you're using npm$ npm init
Zacznijmy od dodania zależności typescript i test — mokka i chai:
$ yarn add --dev @types/chai @types/mocha chai mocha ts-node typescript# lub jeśli używasz npm $ npm install @types/chai @types/mocha chai mocha ts-node typescript --save-dev
Teraz dodajmy Waffle
i ethers
:
$ yarn add --dev ethereum-waffle ethers# or if you're using npm$ npm install ethereum-waffle ethers --save-dev
Twoja struktura projektu powinna teraz wyglądać tak:
1.2├── contracts3├── package.json4└── test5
2. Inteligentny kontrakt
Aby rozpocząć dynamiczne tworzenie atrapy, potrzebujemy inteligentnego kontraktu z zależnościami. Nie martw się, pomyślałem o tym!
Oto prosty inteligentny kontrakt napisany w Solidity
, którego jedynym celem jest sprawdzenie, czy jesteśmy bogaci. Używa tokena ERC20 do sprawdzenia, czy mamy wystarczającą ilość tokenów. Umieść go w ./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}20Pokaż wszystkoKopiuj
Ponieważ chcemy używać dynamicznego tworzenia atrap, nie potrzebujemy całego ERC20, dlatego używamy interfejsu IERC20 z tylko jedną funkcją.
Nadszedł czas, aby zbudować ten kontrakt! W tym celu użyjemy Waffle
. Najpierw stworzymy prosty plik konfiguracyjny waffle.json
, który określa opcje kompilacji.
1{2 "compilerType": "solcjs",3 "compilerVersion": "0.6.2",4 "sourceDirectory": "./contracts",5 "outputDirectory": "./build"6}7Kopiuj
Teraz jesteśmy gotowi zbudować kontrakt z Waffle:
$ npx waffle
Łatwe, prawda? W folderze build/
pojawiły się dwa pliki odpowiadające umowie i interfejsowi. Wykorzystamy je później do testowania.
3. Testowanie
Utwórzmy plik o nazwie AmIRichAlready.test.ts
dla bieżącego testu. Przede wszystkim musimy poradzić sobie z importem. Będziemy ich potrzebować na później:
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
Z wyjątkiem zależności JS, musimy zaimportować naszą wbudowaną umowę i interfejs:
1import IERC20 from "../build/IERC20.json"2import AmIRichAlready from "../build/AmIRichAlready.json"3
Waffle używa chai
do testowania. Zanim jednak będziemy mogli go użyć, musimy wstrzyknąć wyrażenie matcher Waffle do samego chai:
1use(solidity)2
Musimy zaimplementować funkcję beforeEach()
, która zresetuje stan kontraktu przed każdym testem. Zastanówmy się najpierw nad tym, czego tam potrzebujemy. Aby wdrożyć umowę, potrzebujemy dwóch rzeczy: portfela i wdrożonego kontraktu ERC20, aby przekazać go jako argument dla kontraktu AmIRichAlready
.
Po pierwsze, tworzymy portfel:
1const [wallet] = new MockProvider().getWallets()2
Następnie musimy wdrożyć umowę ERC20. Oto trudna część - mamy tylko interfejs. Jest to ta część, w której Waffle nas ratuje. Waffle posiada magiczną funkcję wdrożenieMockContract()
, która tworzy kontrakt wykorzystujący tylko abi interfejsu:
1const mockERC20 = await deployMockContract(wallet, IERC20.abi)2
Teraz, zarówno z portfelem, jak i z ERC20, możemy kontynuować i wdrożyć kontrakt AmIRichAlready
:
1const contract = await deployContract(wallet, AmIRichAlready, [2 mockERC20.address,3])4
Po tym wszystkim nasza funkcja beforeEach()
została zakończona. Jak dotąd plik AmIRichAlready.test.ts
powinien wyglądać tak:
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})26Pokaż wszystko
Zapiszmy pierwszy test do kontraktu AmIRichAlready
. Czy uważasz, że powinniśmy mieć na uwadze nasz test? Tak, masz rację! Powinniśmy sprawdzić, czy już jesteśmy bogaci :)
Ale poczekaj sekundę. Jak nasz pozorowany kontrakt będzie wiedział, jakie wartości należy zwrócić? Nie zaimplementowaliśmy żadnej logiki dla funkcji balanceOf()
. Jeszcze raz Waffle może tu pomóc. Nasz pozorowany kontrakt ma teraz kilka nowych, fantazyjnych rzeczy:
1await mockERC20.mock.<nameOfMethod>.returns(<value>)2await mockERC20.mock.<nameOfMethod>.withArgs(<arguments>).returns(<value>)3
Dzięki tej wiedzy możemy wreszcie napisać nasz pierwszy 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
Podzielmy ten test na części:
- Ustawiliśmy naszą próbną umowę ERC20 tak, aby zawsze zwracała saldo 999999 tokenów.
- Sprawdź, czy metoda
contract.check()
zwracafalse
.
Jesteśmy gotowi wystrzelić z grubej rury:
Tak więc test działa, ale... wciąż jest trochę miejsca na ulepszenia. Funkcja balanceOf()
zawsze zwróci 99999. Możemy ją ulepszyć poprzez określenie portfela, dla którego funkcja powinna zwracać coś — tak jak prawdziwy kontrakt:
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})7
Jak dotąd przetestowaliśmy tylko przypadek, w którym nie jesteśmy wystarczająco bogaci. Przetestujmy przeciwnie:
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})7
Uruchomiłeś testy...
...i tu jesteś! Nasza umowa wydaje się działać zgodnie z zamierzeniem :)
Testowanie wywołań kontraktów
Podsumujmy dotychczasowe osiągnięcia. Przetestowaliśmy funkcjonalność naszego kontraktu AmIRichAlready
i wygląda na to, że działa poprawnie. To znaczy, że skończyliśmy, prawda? Nie całkiem! Waffle pozwala nam jeszcze bardziej przetestować nasz kontrakt. Ale jak dokładnie? No cóż, w arsenale Waffle'a znajduje się calledOnContract()
i wyrażenia matcher calledOnContractWith()
. Umożliwią nam one sprawdzenie, czy nasz kontrakt wywołał pozorowany kontrakt ERC20. Oto podstawowy test z jednym z tych wyrażeń:
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
Możemy pójść jeszcze dalej i ulepszyć ten test za pomocą innego wyrażenia matcher, o którym wam mówiłem:
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})8
Sprawdźmy, czy testy są poprawne:
Świetnie, wszystkie testy są zielone.
Testowanie połączeń kontraktowych z Waffle jest bardzo łatwe. I oto najlepsza część. Te wyrażenia matcher działają zarówno z normalnymi, jak i próbnymi kontraktami! Wynika to z tego, że Waffle rejestruje i filtruje połączenia EVM zamiast wstrzykiwać kod, tak jak w przypadku popularnych bibliotek testowych dla innych technologii.
Meta
Gratulacje! Teraz wiesz jak korzystać z Waffle do dynamicznego testowania połączeń i modelowania kontraktów. Istnieją o wiele bardziej interesujące funkcje, które należy odkryć. Zalecam nurkowanie w dokumentacji Waffle.
Dokumentacja Waffle'a jest dostępna tutaj.
Kod źródłowy dla tego samouczka można znaleźć tutaj.
Samouczki mogą być interesujące: