Przejdź do głównej zawartości

Waffle: Dynamiczne tworzenie atrap i testowanie wywołań kontraktów

waffleinteligentne kontraktysoliditytestowanietworzenie atrap
Średnie
Daniel Izdebski
14 listopada 2020
6 minuta czytania minute read

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 i TypeScript
  • 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├── contracts
3├── package.json
4└── test

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;
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}
Pokaż wszystko
Kopiuj

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

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"

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"

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)

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

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)

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

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"
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})
Pokaż 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>)

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

Podzielmy ten test na części:

  1. Ustawiliśmy naszą próbną umowę ERC20 tak, aby zawsze zwracała saldo 999999 tokenów.
  2. Sprawdź, czy metoda contract.check() zwraca false.

Jesteśmy gotowi wystrzelić z grubej rury:

Jeden test zaliczony

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

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

Uruchomiłeś testy...

Zaliczenie dwóch testów

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

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.balanceOf
3 .withArgs(wallet.address)
4 .returns(utils.parseEther("999999"))
5 await contract.check()
6 expect("balanceOf").to.be.calledOnContractWith(mockERC20, [wallet.address])
7})

Sprawdźmy, czy testy są poprawne:

Zaliczenie trzech testów

Ś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(opens in a new tab).

Kod źródłowy dla tego samouczka można znaleźć tutaj(opens in a new tab).

Samouczki mogą być interesujące:

  • Testowanie inteligentnych kontraktów z Waffle

Ostatnia edycja: @nhsz(opens in a new tab), 27 lutego 2024

Czy ten samouczek był pomocny?