Как тестировать смарт-контракты
Условия смарт-контракта нельзя изменить. Поэтому всякий раз, когда вы создаёте смарт-контракт, нужно убедиться, что он работает правильно. Тестирование — безопасный способ проверить контракт в разных ситуациях. В этом туториале вы узнаете, какие шаги для этого нужно предпринять.
Я расскажу:
- Как подготовить тестовую среду.
- Как написать тесты на JavaScript и выполнить их в Truffle.
Туториал рассчитан на новичков, которые только начали углубляться в разработку и тестирование смарт-контрактов на Ethereum. Чтобы разобраться в этом туториале, у вас должен быть хотя бы базовый уровень знаний по JavaScript и Solidity.
1. Как подготовить тестовую среду
Существует много способов тестирования смарт-контрактов, но Truffle — самый популярный инструмент для написания тестов с использованием JavaScript. На Truffle можно как писать юнит-тесты, так и проводить полноценное интеграционное тестирование с реальными параметрами из продакшн среды.
Для начала нужно установить последнюю версию Node.js с официального сайта.
Затем открыть терминал и установить Truffle, используя следующую команду:
npm install -g truffle
После установки Truffle, не закрывая терминал, создайте каталог Funding:
mkdir Funding
Далее, переходим в каталог командой:
cd Funding
Для инициализации каталога выполните команду:
truffle init
После выполнения команды в каталоге Funding будут созданы следующие папки и файлы:
Дальше мы поработаем с каждым каталогом. А пока продолжим подготавливать тестовую среду.
Для запуска тестов понадобятся Javascript-библиотеки.
Mocha — библиотека, которая содержит общие функции для тестирования, включая describe и it.
Chai — библиотека, которая поддерживает разнообразные функции для проверок. Есть разные «стили» проверки результатов, и Chai предоставляет нам эту возможность. В туториале будем использовать Should.
По умолчанию Mocha входит в состав Truffle, мы можем без проблем использовать функции библиотеки. Chai необходимо установить вручную. Для этого используйте терминал и в корневом каталоге проекта выполните команду:
npm install chai
Также я установил библиотеку chai-bignumber для сравнения чисел с произвольной точностью:
npm install --save-dev chai-bignumber
На этом тестовая среда готова. Теперь можно приступить к разработке смарт-контракта и тестов для него.
2. Как написать тесты на JavaScript и выполнить их в Truffle
Для разработки тестов понадобится смарт-контракт. Мы разработаем контракт, который позволяет собирать пожертвования, задавать конкретную сумму для достижения сбора и выводить средства. Если кто-то пожертвует больше, то ему будет возвращена разница между суммой, которая набралась, и суммой, которую нужно собрать.
Перейдем в каталог Funding → contracts. В Funding/contracts создадим файл Funding.sol с расширением .sol — это и будет смарт-контракт для тестирования.
В Funding.sol добавим следующий код:
pragma solidity 0.4.24;
contract Funding {
uint public raised;
uint public goal;
address public owner;
event Donated(uint donation);
event Withdrew(uint amount);
modifier onlyOwner() {
require(owner == msg.sender);
_;
}
modifier isFinished() {
require(isFunded());
_;
}
modifier notFinished() {
require(!isFunded());
_;
}
constructor (uint _goal) public {
owner = msg.sender;
goal = _goal;
}
function isFunded() public view returns (bool) {
return raised >= goal;
}
function donate() public payable notFinished {
uint refund;
raised += msg.value;
if (raised > goal) {
refund = raised - goal;
raised -= refund;
msg.sender.transfer(refund);
}
emit Donated(msg.value);
}
function withdraw() public onlyOwner isFinished {
uint amount = address(this).balance;
owner.transfer(amount);
emit Withdrew(amount);
}
}
Контракт готов. Развернем смарт-контракт посредством миграции.
Миграции — это файлы JavaScript, которые помогут вам развернуть контракты в сети Ethereum. Это основной способ развертывания контрактов.
Перейдем в каталог Funding → migrations и создадим файл 2_funding.js, добавив в него следующий код:
const Funding = artifacts.require("./Funding.sol");
const ETHERS = 10**18;
const GOAL = 20 * ETHERS;
module.exports = function(deployer) {
deployer.deploy(Funding, GOAL);
};
Чтобы запустить тесты, нужно использовать команду truffle test. В терминале переходим в корень каталога Funding, который создали при подготовке тестовой среды и вводим:
truffle test
Если в терминале появится следующий вывод, то все сделано верно:
Теперь приступим к написанию тестов.
Проверка владельца
Теперь перейдем в каталог Funding → test и создадим файл test_funding.js с расширением .js. Это файл, в котором будут написаны тесты.
В файл test_funding.js нужно добавить следующий код:
const Funding = artifacts.require("Funding");
require("chai").use(require("chai-bignumber")(web3.BigNumber)).should();
contract("Funding", function([account, firstDonator, secondDonator]) {
const ETHERS = 10**18;
const GAS_PRICE = 10**6;
let fundingContract = null;
it("should check the owner is valid", async () => {
fundingContract = await Funding.deployed();
const owner = await fundingContract.owner.call()
owner.should.be.bignumber.equal(account);
});
В тесте мы проверяем, что контракт Funding хранит адрес владельца, который развернул контракт. В нашем случае account — это первый элемент массива. Truffle позволяет использовать до десяти адресов, в тестах нам понадобится всего три адреса.
Приём пожертвований и проверка окончания сбора средств
В этом разделе мы проверим:
- Приём и сумму пожертвований.
- Достигнута ли определенная сумма пожертвования.
- Что произойдет, если пожертвовать больше, чем нужно собрать.
- Общую сумму пожертвования.
- Можно ли продолжить сбор средств, если нужная сумма собрана.
Напишем и разберем тесты:
. . . . . . . . . . . . . . . . . . . . . .
const ETHERS = 10**18;
const GAS_PRICE = 10**6;
let fundingContract = null;
let txEvent;
function findEvent(logs, eventName) {
let result = null;
for (let log of logs) {
if (log.event === eventName) {
result = log;
break;
}
}
return result;
};
it("should accept donations from the donator #1", async () => {
const bFirstDonator= web3.eth.getBalance(firstDonator);
const donate = await fundingContract.donate({
from: firstDonator,
value: 5 * ETHERS,
gasPrice: GAS_PRICE
});
txEvent = findEvent(donate.logs, "Donated");
txEvent.args.donation.should.be.bignumber.equal(5 * ETHERS);
const difference = bFirstDonator.sub(web3.eth.getBalance(firstDonator)).sub(new web3.BigNumber(donate.receipt.gasUsed * GAS_PRICE));
difference.should.be.bignumber.equal(5 * ETHERS);
});
Перед разбором теста, хочу отметить 2 момента:
- Для поиска событий (events) и проверки его аргументов была написана небольшая функция findEvent.
- Для удобства тестирования и расчетов была выставлена собственная стоимость за gas (константа GAS_PRICE).
Теперь разберем тест. В тесте мы проверили:
- что можем принимать пожертвования, вызвав метод donate ();
- что правильно указана сумма, которую нам пожертвовали;
- что у того, кто пожертвовал средства, баланс уменьшился на пожертвованную сумму.
it("should check if donation is not completed", async () => {
const isFunded = await fundingContract.isFunded();
isFunded.should.be.equal(false);
});
В этом тесте проверили, что сбор средств ещё не завершился.
it("should not allow to withdraw the fund until the required amount has
been collected", async () => {
let isCaught = false;
try {
await fundingContract.withdraw({ gasPrice: GAS_PRICE });
} catch (err) {
isCaught = true;
}
isCaught.should.be.equal(true);
});
В тесте проверили, что не можем выводить средства, пока нужная нам сумма не будет собрана.
it("should accept donations from the donator #2", async () => {
const bSecondDonator= web3.eth.getBalance(secondDonator);
const donate = await fundingContract.donate({
from: secondDonator,
value: 20 * ETHERS,
gasPrice: GAS_PRICE
});
txEvent = findEvent(donate.logs, "Donated");
txEvent.args.donation.should.be.bignumber.equal(20 * ETHERS);
const difference = bSecondDonator.sub(web3.eth.getBalance(secondDonator)).sub(new web3.BigNumber(donate.receipt.gasUsed * GAS_PRICE));
difference.should.be.bignumber.equal(15 * ETHERS);
});
В тесте проверили, что если пожертвовать большую сумму, то метод donate () рассчитывает и возвращает средства тому, кто пожертвовал больше, чем нужно. Эта сумма равна разнице между суммой, которая набралась и суммой, которую требуется собрать.
it("should check if the donation is completed", async () => {
const notFunded = await fundingContract.isFunded();
notFunded.should.be.equal(true);
});
it("should check if donated amount of money is correct", async () => {
const raised = await fundingContract.raised.call();
raised.should.be.bignumber.equal(20 * ETHERS);
});
it("should not accept donations if the fundraising is completed", async () => {
let isCaught = false;
try {
await fundingContract.donate({ from: firstDonator, value: 10 * ETHERS });
} catch (err) {
isCaught = true;
}
isCaught.should.be.equal(true);
});
В этих трёх тестах мы проверили:
- что сбор средств завершен;
- что сумма пожертвования правильная;
- что больше никто не может пожертвовать, так как сбор средств завершен.
Вывод средств
В предыдущем разделе туториала, мы собрали нужную нам сумму, теперь её можно вывести:
. . . . . . . . . . . . . . . . . . . . . .
it("should allow the owner to withdraw the fund", async () => {
const bAccount = web3.eth.getBalance(account);
const withdraw = await fundingContract.withdraw({ gasPrice: GAS_PRICE });
txEvent = findEvent(withdraw.logs, "Withdrew");
txEvent.args.amount.should.be.bignumber.equal(20 * ETHERS);
const difference = web3.eth.getBalance(account).sub(bAccount);
difference.should.be.bignumber.equal(await fundingContract.raised.call() - withdraw.receipt.gasUsed * GAS_PRICE);
});
Вызвав функцию withdraw (), мы вывели средства владельца смарт-контракта, в нашем случае это account. Потом проверили, что действительно вывели нужную нам сумму. Для этого в константу difference записали разницу баланса до и после вывода средств. Полученный результат сравнили с суммой пожертвований минус плата за транзакцию. Как упоминалось выше, для удобства тестирования и расчетов, я установил собственную цену за gas.
Запустим написанные тесты командой truffle test. Если все сделали правильно, результат должен быть следующим:
Результат
Я попытался простым и понятным языком описать шаги тестирования смарт-контрактов: от подготовки тестовой среды до написания самих тестов.
Теперь вы сможете протестировать любой смарт-контракт и убедиться, что он работает правильно.