Lanjut ke konten utama

Waffle: Mocking dinamis dan percobaan memanggil kontrak pintar

wafflekontrak pintarsoliditypengujianmocking
Tingkat menengah
Daniel Izdebski
14 November 2020
6 bacaan singkat minute read

Apa yang akan di pelajari pada tutorial ini?

Pada tutorial ini, Anda akan belajar cara:

  • menggunakan mocking dinamis
  • menguji interaksi antara kontrak pintar

Asumsi:

  • Anda telah mengetahui bagaimana cara menulis kontrak pintar sederhana dalam Solidity
  • Anda terbiasa dengan JavaScript dan TypeScript
  • Anda telah menyelesaikan tutorial Waffle lainnya atau mengetahui beberapa hal tentangnya

Mocking dinamis

Mengapa mocking dinamis sangat berguna? Baiklah, hal ini mengizinkan kita menulis tes unit ketimbang tes yang terintegrasi. Apa maksudnya? Hal ini berarti bahwa kita tidak perlu khawatir tentang dependensi kontrak pintar, sehingga kita bisa menguji semuanya secara terisolasi. Saya akan menunjukan bagaimana cara Anda melakukannya.

1. Proyek

Sebelum memulai, kita perlu menyiapkan proyek node.js sederhana:

$ mkdir dynamic-mocking
$ cd dynamic-mocking
$ mkdir contracts src
$ yarn init
# or if you're using npm
$ npm init

Mari kita mulai dengan menambah typescript dan menguji dependensi - mocha dan chai:

$ yarn add --dev @types/chai @types/mocha chai mocha ts-node typescript
# atau jika anda menggunakan npm
$ npm install @types/chai @types/mocha chai mocha ts-node typescript --save-dev

Sekarang mari tambahkan Waffle dan ether:

$ yarn add --dev ethereum-waffle ethers
# atau jika anda menggunakan npm
$ npm install ethereum-waffle ethers --save-dev

Struktur proyek Anda seharusnya terlihat seperti ini:

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

2. Kontrak pintar

Untuk memulai mocking dinamis, kita membutuhkan kontrak pintar dengan dependensi. Tenang, saya akan membantu!

Berikut adalah kontrak pintar sederhana yang ditulis dalam Solidity yang tujuannya hanya memeriksa apakah kita kaya. Kontrak pintar ini menggunakan token ERC20 untuk memeriksa apakah kita memiliki token yang cukup. Letakan pada ./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}
Tampilkan semua
Salin

Karena kita mau menggunakan mocking dinamis, kita tidak memerlukan seluruh ERC20, itulah mengapa kita menggunakan antarmuka IERC20 dengan hanya satu fungsi.

Saatnya membuat kontrak ini! Untuk itu, kita akan menggunakan Waffle. Pertama, kita akan membuat file konfigurasi waffle.json sederhana yang menentukan pilihan kompilasi.

1{
2 "compilerType": "solcjs",
3 "compilerVersion": "0.6.2",
4 "sourceDirectory": "./contracts",
5 "outputDirectory": "./build"
6}
Salin

Sekarang, kita sudah siap untuk membuat kontrak menggunakan Waffle:

$ npx waffle

Mudah, bukan? Dalam folder build/, dua file sesuai dengan kontrak dan antar mukanya muncul. Kita akan menggunakannya nanti untuk percobaan.

3. Pengujian

Mari kita buat file bernama AmIRichAlready.test.ts untuk tes yang sebenarnya. Pertama, kita harus menangani hasil impor. Kita akan membutuhkannya nanti:

1import { expect, use } from "chai"
2import { Contract, utils, Wallet } from "ethers"
3import {
4 deployContract,
5 deployMockContract,
6 MockProvider,
7 solidity,
8} from "ethereum-waffle"

Kecuali untuk dependensi JS, kita harus mengimpor kontrak dan antar muka yang kita bangun:

1import IERC20 from "../build/IERC20.json"
2import AmIRichAlready from "../build/AmIRichAlready.json"

Waffle menggunakan chai untuk pengujian. Namun, sebelum kita menggunakannya, kita harus menginjeksi matcher Waffle kedalam chai:

1use(solidity)

Kita harus mengimplementasikan fungsi beforeEach() yang akan mengatur ulang state kontrak sebelum setiap tes dimulai. Pertama-tama mari kita pikirkan apa yang kita butuhkan untuk itu. Untuk menggunakan kontrak, kita membutuhkan dua hal: dompet dan kontrak ERC20 yang digunakan untuk meneruskannya sebagai argumen kontrak AmIRichAlready.

Pertama, kita buat dompetnya:

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

Selanjutnya, kita harus menggunakan kontrak ERC20. Ini bagian sulitnya - kita hanya memiliki satu antarmuka. Di sinilah peran di mana Waffle datang menyelamatkan kita. Waffle memiliki fungsi deployMockContract() ajaib yang membuat kontrak dengan hanya menggunakan abi dari antarmuka:

1const mockERC20 = await deployMockContract(wallet, IERC20.abi)

Sekarang dengan dompet maupun ERC20 yang digunakan, kita akan lanjutkan dan menggunakan kontrak AmIRichAlready:

1const contract = await deployContract(wallet, AmIRichAlready, [
2 mockERC20.address,
3])

Secara keseluruhan, fungsi beforeEach() kita telah selesai. Sejauh ini file AmIRichAlready.test.ts Anda seharusnya telihat seperti ini:

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

Mari kita tulis tes pertama ke kontrak AmIRichAlready. Menurut Anda, tentang apa seharusnya tes kita? Ya, Anda benar! Kita harus memeriksa apakah kita sudah kaya :)

Tapi tunggu dulu. Bagaimana kontrak mocked kita tahu nilai apa yang dikembalikan? Kita belum mengimplementasikan logika apa pun untuk fungsi balanceOf(). Sekali lagi, Waffle bisa membantu kita di sini. Kontrak mocked kita memiliki hal baru yang menarik sekarang:

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

Dengan pengetahuan ini, akhirnya kita bisa menulis tes pertama kita:

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

Mari kita pecahkan tes ini ke dalam bagian-bagian:

  1. Kita atur kontrak mock ERC20 kita untuk selalu mengembalikan saldo 999999 token.
  2. Periksa apakah metode contract.check() mengembalikan nilai false.

Kita siap untuk menyalakannya:

Satu ujian lulus

Jadi testnya bekerja, namun... masih ada sedikit ruang untuk peningkatan. Fungsi balanceOf() akan selalu mengembalikan 99999. Kita bisa meningkatkannya dengan menentukan dompet yang ke mana fungsinya harus mengembalikan sesuatu - sama seperti kontrak sungguhan:

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

Sejauh ini, kita hanya mencoba kasus di mana kita tidak cukup kaya. Mari kita coba kebalikannya:

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

Anda jalankan tesnya...

Dua ujian lulus

....dan ini dia! Kontrak kita tampak berjalan sebagaimana mestinya :)

Menguji pemanggilan kontrak

Mari kita ringkas apa yang telah kita lakukan sejauh ini. Kita telah menguji fungsionalitas kontrak AmIRichAlready kita dan tampaknya bekerja dengan benar. Artinya, kita telah selesai, bukan? Belum selesai! Waffle memungkinkan kita menguji kontrak bahkan lebih jauh lagi. Tapi, bagaimana persisnya? Dalam gudang senjata Waffle, ada matcher calledOnContract() dan calledOnContractWith(). Mereka akan memungkinkan kita memeriksa apakah kontrak kita memanggil kontrak mock ERC20. Berikut adalah tes dasarnya dengan salah satu matcher ini:

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

Kita bahkan bisa melangkah lebih jauh dan meningkatkan tes ini dengan matcher lain yang saya sebutkan sebelumnya:

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

Mari kita periksa apakah tesnya benar:

Tiga ujian lulus

Hebat, semua test berwarna hijau.

Menguji pemanggilan kontrak dengan Waffle sangatlah mudah. Dan inilah bagian terbaiknya. Matcher ini bekerja baik dalam kontrak normal dan mocked! Itu karena Waffle mencatat dan menyaring pemanggilan EVM ketimbang menginjeksi kode, seperti dalam kasus pustaka pengujian populer untuk teknologi lainnya.

Garis Akhir

Selamat! Sekarang Anda tahu cara menggunakan Waffle untuk menguji pemanggilan kontrak dan kontrak mock secara dinamis. Ada fitur yang jauh lebih menarik untuk ditemukan. Saya menyarankan menyelam ke dalam dokumentasi Waffle.

Dokumentasi Waffle tersedia di sini(opens in a new tab).

Sumber kode untuk tutorial ini bisa ditemukan di sini(opens in a new tab).

Tutorial yang mungkin juga Anda minati:

  • Menguji kontrak pintar dengan Waffle

Terakhir diedit: @nhsz(opens in a new tab), 27 Februari 2024

Apakah tutorial ini membantu?