Перейти до основного контенту

Waffle: динамічне мокування та тестування викликів контрактів

waffle
Смарт-контракти
мова програмування
тестування
глузливий
Середнячок
Daniel Izdebski
14 листопада 2020 р.
6 читається за хвилину

Про що цей підручник?

У цьому підручнику ви дізнаєтеся, як:

  • використовувати динамічне мокування
  • тестувати взаємодію між смарт-контрактами

Припущення:

  • ви вже знаєте, як написати простий смарт-контракт на Solidity
  • ви добре орієнтуєтеся в JavaScript та TypeScript
  • ви пройшли інші підручники з Waffle або вже дещо про нього знаєте

Динамічне мокування

Чому динамічне мокування корисне? Ну, це дозволяє нам писати модульні тести замість інтеграційних тестів. Що це означає? Це означає, що нам не потрібно турбуватися про залежності смарт-контрактів, тому ми можемо тестувати їх усі в повній ізоляції. Дозвольте мені показати вам, як саме ви можете це зробити.

1. Проєкт

Перш ніж розпочати, нам потрібно підготувати простий проєкт node.js:

mkdir dynamic-mocking
cd dynamic-mocking
mkdir contracts src
yarn init
# або якщо ви використовуєте npm
npm init

Почнімо з додавання typescript і тестових залежностей — mocha та chai:

yarn add --dev @types/chai @types/mocha chai mocha ts-node typescript
# або якщо ви використовуєте npm
npm install @types/chai @types/mocha chai mocha ts-node typescript --save-dev

Тепер додамо Waffle та ethers:

yarn add --dev ethereum-waffle ethers
# або якщо ви використовуєте npm
npm install ethereum-waffle ethers --save-dev

Тепер структура вашого проєкту має виглядати так:

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

2. Смарт-контракт

Щоб розпочати динамічне мокування, нам потрібен смарт-контракт із залежностями. Не хвилюйтеся, я про все подбав!

Ось простий смарт-контракт, написаний на Solidity, єдина мета якого — перевірити, чи ми багаті. Він використовує токен ERC20, щоб перевірити, чи достатньо у нас токенів. Помістіть його в ./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}
Показати все

Оскільки ми хочемо використовувати динамічне мокування, нам не потрібен весь ERC20, тому ми використовуємо інтерфейс IERC20 лише з однією функцією.

Час зібрати цей контракт! Для цього ми будемо використовувати Waffle. Спочатку ми створимо простий конфігураційний файл waffle.json, який визначає параметри компіляції.

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

Тепер ми готові зібрати контракт за допомогою Waffle:

npx waffle

Легко, правда? У папці build/ з'явилися два файли, що відповідають контракту та інтерфейсу. Ми будемо використовувати їх пізніше для тестування.

3. Тестування

Створімо файл під назвою AmIRichAlready.test.ts для фактичного тестування. Перш за все, нам потрібно налаштувати імпорти. Вони нам знадобляться пізніше:

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

Окрім залежностей JS, нам потрібно імпортувати наш зібраний контракт та інтерфейс:

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

Waffle використовує chai для тестування. Однак, перш ніж ми зможемо його використовувати, нам потрібно впровадити матчери Waffle у сам chai:

1use(solidity)

Нам потрібно реалізувати функцію beforeEach(), яка скидатиме стан контракту перед кожним тестом. Давайте спочатку подумаємо, що нам там потрібно. Щоб розгорнути контракт, нам потрібні дві речі: гаманець і розгорнутий контракт ERC20, щоб передати його як аргумент контракту AmIRichAlready.

Перш за все, ми створюємо гаманець:

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

Тоді нам потрібно розгорнути контракт ERC20. Ось складна частина — у нас є лише інтерфейс. Саме тут Waffle приходить нам на допомогу. Waffle має магічну функцію deployMockContract(), яка створює контракт, використовуючи лише _abi_ інтерфейсу:

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

Тепер, маючи і гаманець, і розгорнутий ERC20, ми можемо розгортати контракт AmIRichAlready:

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

На цьому наша функція beforeEach() готова. Поки що ваш файл AmIRichAlready.test.ts має виглядати так:

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})
Показати все

Напишімо перший тест для контракту AmIRichAlready. Як ви гадаєте, про що має бути наш тест? Так, ви маєте рацію! Ми повинні перевірити, чи ми вже багаті :)

Але почекайте секунду. Звідки наш змокований контракт знатиме, які значення повертати? Ми не реалізували жодної логіки для функції balanceOf(). Знову ж таки, Waffle може тут допомогти. Тепер наш змокований контракт має кілька нових цікавих можливостей:

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

Маючи ці знання, ми нарешті можемо написати наш перший тест:

1it("повертає false, якщо в гаманці менше ніж 1000000 токенів", async () => {
2 await mockERC20.mock.balanceOf.returns(utils.parseEther("999999"))
3 expect(await contract.check()).to.be.equal(false)
4})

Розберемо цей тест по частинах:

  1. Ми налаштували наш змокований контракт ERC20 так, щоб він завжди повертав баланс у 999999 токенів.
  2. Перевірте, чи метод contract.check() повертає false.

Ми готові все запустити:

Один пройдений тест

Отже, тест працює, але... ще є простір для вдосконалення. Функція balanceOf() завжди повертатиме 99999. Ми можемо покращити його, вказавши гаманець, для якого функція повинна щось повертати — так само, як справжній контракт:

1it("повертає false, якщо в гаманці менше ніж 1000001 токенів", 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})

Досі ми тестували лише випадок, коли ми недостатньо багаті. Натомість протестуймо протилежний випадок:

1it("повертає true, якщо в гаманці є щонайменше 1000001 токенів", 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})

Ви запускаєте тести...

Два пройдених тести

...і готово! Здається, наш контракт працює належним чином :)

Тестування викликів контрактів

Підсумуймо, що ми вже зробили. Ми протестували функціонал нашого контракту AmIRichAlready, і він, здається, працює належним чином. Це означає, що ми закінчили, правда? Не зовсім! Waffle дозволяє нам протестувати наш контракт ще глибше. Але як саме? В арсеналі Waffle є матчери calledOnContract() і calledOnContractWith(). Вони дозволять нам перевірити, чи наш контракт викликав змокований контракт ERC20. Ось базовий тест з одним із цих матчерів:

1it("перевіряє, чи контракт викликав balanceOf для токена ERC20", async () => {
2 await mockERC20.mock.balanceOf.returns(utils.parseEther("999999"))
3 await contract.check()
4 expect("balanceOf").to.be.calledOnContract(mockERC20)
5})

Ми можемо піти ще далі й покращити цей тест за допомогою іншого матчера, про який я вам розповідав:

1it("перевіряє, чи контракт викликав balanceOf з певним гаманцем для токена ERC20", 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})

Давайте перевіримо, чи правильні тести:

Три пройдених тести

Чудово, всі тести зелені.

Тестувати виклики контрактів за допомогою Waffle дуже просто. І ось найкраща частина. Ці матчери працюють як зі звичайними, так і зі змокованими контрактами! Це тому, що Waffle записує та фільтрує виклики EVM, а не впроваджує код, як це відбувається в популярних бібліотеках тестування для інших технологій.

Фінішна пряма

Вітаємо! Тепер ви знаєте, як використовувати Waffle для тестування викликів контрактів і динамічного мокування контрактів. Є ще багато цікавих особливостей, які слід відкрити. Я рекомендую зануритися в документацію Waffle.

Документація Waffle доступна тутopens in a new tab.

Вихідний код для цього підручника можна знайти тутopens in a new tab.

Підручники, які також можуть вас зацікавити:

Останні оновлення сторінки: 27 лютого 2024 р.

Чи була ця інструкція корисною?