
Условия смарт-контракта нельзя изменить. Поэтому всякий раз, когда вы создаёте смарт-контракт, нужно убедиться, что он работает правильно. Тестирование — безопасный способ проверить контракт в разных ситуациях. В этом туториале вы узнаете, какие шаги для этого нужно предпринять.
Я расскажу:
- Как подготовить тестовую среду.
- Как написать тесты на 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. Если все сделали правильно, результат должен быть следующим:

Результат
Я попытался простым и понятным языком описать шаги тестирования смарт-контрактов: от подготовки тестовой среды до написания самих тестов.
Теперь вы сможете протестировать любой смарт-контракт и убедиться, что он работает правильно.
