Waffle: динамічне мокування та тестування викликів контрактів
Про що цей підручник?
У цьому підручнику ви дізнаєтеся, як:
- використовувати динамічне мокування
- тестувати взаємодію між смарт-контрактами
Припущення:
- ви вже знаєте, як написати простий смарт-контракт на
Solidity - ви добре орієнтуєтеся в
JavaScriptтаTypeScript - ви пройшли інші підручники з
Waffleабо вже дещо про нього знаєте
Динамічне мокування
Чому динамічне мокування корисне? Ну, це дозволяє нам писати модульні тести замість інтеграційних тестів. Що це означає? Це означає, що нам не потрібно турбуватися про залежності смарт-контрактів, тому ми можемо тестувати їх усі в повній ізоляції. Дозвольте мені показати вам, як саме ви можете це зробити.
1. Проєкт
Перш ніж розпочати, нам потрібно підготувати простий проєкт node.js:
mkdir dynamic-mockingcd dynamic-mockingmkdir contracts srcyarn init# або якщо ви використовуєте npmnpm initПочнімо з додавання typescript і тестових залежностей — mocha та chai:
yarn add --dev @types/chai @types/mocha chai mocha ts-node typescript# або якщо ви використовуєте npmnpm install @types/chai @types/mocha chai mocha ts-node typescript --save-devТепер додамо Waffle та ethers:
yarn add --dev ethereum-waffle ethers# або якщо ви використовуєте npmnpm install ethereum-waffle ethers --save-devТепер структура вашого проєкту має виглядати так:
1.2├── contracts3├── package.json4└── test2. Смарт-контракт
Щоб розпочати динамічне мокування, нам потрібен смарт-контракт із залежностями. Не хвилюйтеся, я про все подбав!
Ось простий смарт-контракт, написаний на Solidity, єдина мета якого — перевірити, чи ми багаті. Він використовує токен ERC20, щоб перевірити, чи достатньо у нас токенів. Помістіть його в ./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}Показати всеОскільки ми хочемо використовувати динамічне мокування, нам не потрібен весь 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"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})Показати всеНапишімо перший тест для контракту 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})Розберемо цей тест по частинах:
- Ми налаштували наш змокований контракт ERC20 так, щоб він завжди повертав баланс у 999999 токенів.
- Перевірте, чи метод
contract.check()повертаєfalse.
Ми готові все запустити:
Отже, тест працює, але... ще є простір для вдосконалення. Функція balanceOf() завжди повертатиме 99999. Ми можемо покращити його, вказавши гаманець, для якого функція повинна щось повертати — так само, як справжній контракт:
1it("повертає false, якщо в гаманці менше ніж 1000001 токенів", async () => {2 await mockERC20.mock.balanceOf3 .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.balanceOf3 .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.balanceOf3 .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 р.


