Статьи

Трюфель: тестирование смарт-контрактов

В нашем введении к Truffle мы обсудили, что такое Truffle и как он может помочь вам автоматизировать работу по составлению, тестированию и развертыванию интеллектуальных контрактов.

В этой статье мы рассмотрим, как тестировать умные контракты. Тестирование является наиболее важным аспектом разработки качественного умного контракта.

Зачем тестировать экстенсивно? Таким образом, вы можете избежать подобных вещей или этого . Умные контракты управляют стоимостью, иногда огромной ценностью, что делает их очень интересной добычей для людей, обладающих временем и навыками для нападения на них.

Вы бы не хотели, чтобы ваш проект оказался на кладбище Блокчейн ?

Начиная

Мы собираемся сделать HashMarket , простой рынок подержанных товаров на основе умных контрактов.

Откройте свой терминал и поместите себя в папку, где вы хотите построить проект. В этой папке выполните следующее:

mkdir HashMarket cd HashMarket truffle init 

Вы должны получить результат, который выглядит примерно так:

 Downloading... Unpacking... Setting up... Unbox successful. Sweet! Commands: Compile: truffle compile Migrate: truffle migrate Test contracts: truffle test 

Вы также получите структуру файла, которая выглядит следующим образом:

 . ├── contracts │ └── Migrations.sol ├── migrations │ └── 1_initial_migration.js ├── test ├── truffle-config.js └── truffle.js 

Чтобы освежить в памяти файлы, взгляните на предыдущую статью . В двух словах, у нас есть базовый truffle.js и два файла, используемые для первоначальной миграции на блокчейн.

Подготовка тестовой среды

Самый простой способ проверить это в локальной сети. Я настоятельно рекомендую использовать инструмент TestRPC (ранее известный как TestRPC ) для тестирования по контракту.

Установите ganache-cli (для этого требуется Node Package Manager):

 npm install -g ganache-cli 

После этого откройте отдельное окно терминала или вкладку и запустите:

 ganache-cli 

Вы должны увидеть вывод, похожий на этот:

 Ganache CLI v6.1.0 (ganache-core: 2.1.0) Available Accounts ================== (0) 0xd14c83349da45a12b217988135fdbcbb026ac160 (1) 0xc1df9b406d5d26f86364ef7d449cc5a6a5f2e8b8 (2) 0x945c42c7445af7b3337834bdb1abfa31e291bc40 (3) 0x56156ea86cd46ec57df55d6e386d46d1bbc47e3e (4) 0x0a5ded586d122958153a3b3b1d906ee9ff8b2783 (5) 0x39f43d6daf389643efdd2d4ff115e5255225022f (6) 0xd793b706471e257cc62fe9c862e7a127839bbd2f (7) 0xaa87d81fb5a087364fe3ebd33712a5522f6e5ac6 (8) 0x177d57b2ab5d3329fad4f538221c16cb3b8bf7a7 (9) 0x6a146794eaea4299551657c0045bbbe7f0a6db0c Private Keys ================== (0) 66a6a84ee080961beebd38816b723c0f790eff78f0a1f81b73f3a4c54c98467b (1) fa134d4d14fdbac69bbf76d2cb27c0df1236d0677ec416dfbad1cc3cc058145e (2) 047fef2c5c95d5cf29c4883b924c24419b12df01f3c6a0097f1180fa020e6bd2 (3) 6ca68e37ada9b1b88811015bcc884a992be8f6bc481f0f9c6c583ef0d4d8f1c9 (4) 84bb2d44d64478d1a8b9d339ad1e1b29b8dde757e01f8ee21b1dcbce50e2b746 (5) 517e8be95253157707f34d08c066766c5602e519e93bace177b6377c68cba34e (6) d2f393f1fc833743eb93f108fcb6feecc384f16691250974f8d9186c68a994ef (7) 8b8be7bec3aca543fb45edc42e7b5915aaddb4138310b0d19c56d836630e5321 (8) e73a1d7d659b185e56e5346b432f58c30d21ab68fe550e7544bfb88765235ae3 (9) 8bb5fb642c58b7301744ef908fae85e2d048eea0c7e0e5378594fc7d0030f100 HD Wallet ================== Mnemonic: ecology sweet animal swear exclude quote leopard erupt guard core nice series Base HD Path: m/44'/60'/0'/0/{account_index} Listening on localhost:8545 

Это список всех учетных записей ganache-cli созданных для вас. Вы можете использовать любую учетную запись, какую пожелаете, но эти учетные записи будут предварительно загружены эфиром, что делает их очень полезными (поскольку для тестирования требуется эфир для затрат на газ ).

После этого перейдите к truffle.js или truffle.js truffle-config.js и добавьте в конфигурацию сеть разработки:

 module.exports = { networks: { development: { host: "127.0.0.1", port: 8545, network_id: "*" } } }; 

Написание умного контракта

Первое, что мы сделаем, это напишем умный контракт HashMarket. Мы постараемся сделать его максимально простым, сохраняя при этом необходимую функциональность.

HashMarket — это разновидность eBay на блокчейне. Это позволяет продавцам размещать товары, а покупатели — покупать их для эфира. Это также позволяет продавцам удалять товары, если они не продаются.

В своем проекте в папке contracts создайте новый файл и назовите его HashMarket.sol . В этом файле добавьте следующий код:

 pragma solidity 0.4.21; contract HashMarket { // Track the state of the items, while preserving history enum ItemStatus { active, sold, removed } struct Item { bytes32 name; uint price; address seller; ItemStatus status; } event ItemAdded(bytes32 name, uint price, address seller); event ItemPurchased(uint itemID, address buyer, address seller); event ItemRemoved(uint itemID); event FundsPulled(address owner, uint amount); Item[] private _items; mapping (address => uint) public _pendingWithdrawals; modifier onlyIfItemExists(uint itemID) { require(_items[itemID].seller != address(0)); _; } function addNewItem(bytes32 name, uint price) public returns (uint) { _items.push(Item({ name: name, price: price, seller: msg.sender, status: ItemStatus.active })); emit ItemAdded(name, price, msg.sender); // Item is pushed to the end, so the lenth is used for // the ID of the item return _items.length - 1; } function getItem(uint itemID) public view onlyIfItemExists(itemID) returns (bytes32, uint, address, uint) { Item storage item = _items[itemID]; return (item.name, item.price, item.seller, uint(item.status)); } function buyItem(uint itemID) public payable onlyIfItemExists(itemID) { Item storage currentItem = _items[itemID]; require(currentItem.status == ItemStatus.active); require(currentItem.price == msg.value); _pendingWithdrawals[currentItem.seller] = msg.value; currentItem.status = ItemStatus.sold; emit ItemPurchased(itemID, msg.sender, currentItem.seller); } function removeItem(uint itemID) public onlyIfItemExists(itemID) { Item storage currentItem = _items[itemID]; require(currentItem.seller == msg.sender); require(currentItem.status == ItemStatus.active); currentItem.status = ItemStatus.removed; emit ItemRemoved(itemID); } function pullFunds() public returns (bool) { require(_pendingWithdrawals[msg.sender] > 0); uint outstandingFundsAmount = _pendingWithdrawals[msg.sender]; if (msg.sender.send(outstandingFundsAmount)) { emit FundsPulled(msg.sender, outstandingFundsAmount); return true; } else { return false; } } } 

После того, как вы это сделаете, попробуйте запустить truffle compile чтобы увидеть, truffle compile ли код. Поскольку Solidity имеет тенденцию изменять соглашения, если ваш код не будет компилироваться, вероятное решение использует более старую версию компилятора (0.4.21. Это версия, с которой она была написана, и она будет в порядке).

Написание миграции

Вам нужно написать миграцию, чтобы Truffle знал, как развернуть ваш контракт в блокчейне. Перейдите в папку migrations и создайте новый файл с именем 2_deploy_contracts.js . В этом файле добавьте следующий код:

 var HashMarket = artifacts.require("./HashMarket.sol"); module.exports = function(deployer) { deployer.deploy(HashMarket); }; 

Поскольку у нас только один контракт, файл миграции очень прост.

Теперь запустите truffle migrate и, надеюсь, вы получите что-то вроде этого:

 Using network 'development'. Running migration: 1_initial_migration.js Deploying Migrations... ... 0xad501b7c4e183459c4ee3fee58ea9309a01aa345f053d053b7a9d168e6efaeff Migrations: 0x9d69f4390c8bb260eadb7992d5a3efc8d03c157e Saving successful migration to network... ... 0x7deb2c3d9dacd6d7c3dc45dc5b1c6a534a2104bfd17a1e5a93ce9aade147b86e Saving artifacts... Running migration: 2_deploy_contracts.js Deploying HashMarket... ... 0xcbc967b5292f03af2130fc0f5aaced7080c4851867abd917d6f0d52f1072d91e HashMarket: 0x7918eaef5e6a21a26dc95fc95ce9550e98e789d4 Saving successful migration to network... ... 0x5b6a332306f739b27ccbdfd10d11c60200b70a55ec775e7165358b711082cf55 Saving artifacts... 

Тестирование смарт-контрактов

Вы можете использовать Solidity или JavaScript для умного контрактного тестирования. Надежность может быть немного более интуитивной при тестировании умных контрактов, но JavaScript дает вам гораздо больше возможностей.

Тестирование твердости

Чтобы начать тестирование, в папке test в вашем проекте создайте файл с именем TestHashMarket.sol . Набор Truffle предоставляет нам вспомогательные функции для тестирования, поэтому мы должны импортировать их. В начале файла добавьте:

 pragma solidity ^0.4.20; import "truffle/Assert.sol"; import "truffle/DeployedAddresses.sol"; import "../contracts/HashMarket.sol"; 

Первые два импорта являются важными.

Импорт Assert дает нам доступ к различным функциям тестирования, таким как Assert.equals() , Assert.greaterThan() и т. Д. Таким образом, Assert работает с Truffle для автоматизации наиболее «скучного» написания кода.

Импорт DeployedAddresses управляет адресами контрактов для нас. Поскольку каждый раз, когда вы изменяете свой контракт, вы должны повторно развертывать его на новый адрес, и даже если вы не меняете его, каждый раз, когда вы проверяете контракт, его необходимо развертывать, чтобы начать с первоначального состояния. Библиотека DeployedAddresses управляет этим для нас.

Теперь давайте напишем тест. Под директивами import добавьте следующее:

 contract TestHashMarket { function testAddingNewProduct() public { // DeployedAddresses.HashMarket() handles contract address // management for us HashMarket market = HashMarket(DeployedAddresses.HashMarket()); bytes32 expectedName = "T"; uint expectedPrice = 1000; uint itemID = market.addNewItem(expectedName, expectedPrice); bytes32 name; uint price; address seller; uint status; (name, price, seller, status) = market.getItem(itemID); Assert.equal(name, expectedName, "Item name should match"); Assert.equal(price, expectedPrice, "Item price should match"); Assert.equal(status, uint(HashMarket.ItemStatus.active), "Item status at creation should be .active"); Assert.equal(seller, this, "The function caller should be the seller"); } } 

Давайте посмотрим на некоторые важные части теста. В первую очередь:

 HashMarket market = HashMarket(DeployedAddresses.HashMarket()); 

Этот код использует библиотеку DeployedAddresses для создания нового экземпляра
Контракт HashMarket для тестирования.

 Assert.equal(<current>, <expected>, <message>) 

Эта часть кода проверяет, равны ли два значения. Если да, он передает сообщение об успехе в набор тестов. Если нет, он сообщает об ошибке. Он также добавляет сообщение, чтобы вы могли узнать, где произошел сбой вашего кода.

Теперь давайте запустим это:

 truffle test 

Вы должны получить такой результат:

 TestHashMarket 1) testAddingNewProduct > No events were emitted 0 passing (879ms) 1 failing 1) TestHashMarket testAddingNewProduct: Error: VM Exception while processing transaction: revert at Object.InvalidResponse (/usr/local/lib/node_modules/truffle/build/webpack:/~/web3/lib/web3/errors.js:38:1) at /usr/local/lib/node_modules/truffle/build/webpack:/~/web3/lib/web3/requestmanager.js:86:1 at /usr/local/lib/node_modules/truffle/build/webpack:/~/truffle-provider/wrapper.js:134:1 at XMLHttpRequest.request.onreadystatechange (/usr/local/lib/node_modules/truffle/build/webpack:/~/web3/lib/web3/httpprovider.js:128:1) at XMLHttpRequestEventTarget.dispatchEvent (/usr/local/lib/node_modules/truffle/build/webpack:/~/xhr2/lib/xhr2.js:64:1) at XMLHttpRequest._setReadyState (/usr/local/lib/node_modules/truffle/build/webpack:/~/xhr2/lib/xhr2.js:354:1) at XMLHttpRequest._onHttpResponseEnd (/usr/local/lib/node_modules/truffle/build/webpack:/~/xhr2/lib/xhr2.js:509:1) at IncomingMessage.<anonymous> (/usr/local/lib/node_modules/truffle/build/webpack:/~/xhr2/lib/xhr2.js:469:1) at endReadableNT (_stream_readable.js:1106:12) at process._tickCallback (internal/process/next_tick.js:178:19) 

Наш тест не удался 🙁

Давайте войдем в договор, чтобы проверить возможные ошибки.

После тщательной проверки мы обнаружим, что проблема с нашим контрактом находится в операторе return нашего метода addNewItem :

 function addNewItem(bytes32 name, uint price) public returns (uint) { _items.push(Item({ name: name, price: price, seller: msg.sender, status: ItemStatus.active })); // Item is pushed to the end, so the lenth is used for // the ID of the item return _items.length; } 

Поскольку массивы индексируются нулем, и мы используем позицию массива в качестве идентификатора, мы должны фактически вернуть _items.length - 1 . Исправьте эту ошибку и запустите это снова:

 truffle test 

Вы должны получить гораздо более счастливое сообщение:

 TestHashMarket ✓ testAddingNewProduct (130ms) 1 passing (729ms) 

Мы успешно использовали тестирование Truffle, чтобы исправить очень вероятную ошибку в нашем коде!

Тестирование JavaScript

Truffle позволяет нам использовать JavaScript для тестирования, используя среду тестирования Mocha. Это позволяет вам писать более сложные тесты и получать больше функциональности от вашей среды тестирования.

Хорошо, давайте напишем тест. Сначала в test папке создайте файл и назовите его hashmarket.js .

Первое, что нам нужно сделать, это получить ссылку на наш контракт в JavaScript. Для этого мы будем использовать функцию artifacts.require(...) Трюфеля:

 var HashMarket = artifacts.require("./HashMarket.sol"); 

Теперь, когда у нас есть ссылка на контракт, давайте начнем писать тесты. Для начала мы будем использовать функцию contract предоставленную нам:

 contract("HashMarket", function(accounts) { }); 

Это создает набор тестов для нашего контракта. Теперь для тестирования мы используем Mocha it синтаксис:

 contract("HashMarket", function(accounts) { it("should add a new product", function() { }); }); 

Здесь описывается тест, который мы напишем, и дается сообщение, чтобы мы знали цель теста. Теперь давайте напишем сам тест. В конце файл hashmarket.js должен выглядеть следующим образом. Аргументация объясняется в комментариях к исходному коду:

 var HashMarket = artifacts.require("./HashMarket.sol"); contract("HashMarket", function(accounts) { it("should add a new product", function() { // Set the names of test data var itemName = "TestItem"; var itemPrice = 1000; var itemSeller = accounts[0]; // Since all of our testing functions are async, we store the // contract instance at a higher level to enable access from // all functions var hashMarketContract; // Item ID will be provided asynchronously so we extract it var itemID; return HashMarket.deployed().then(function(instance) { // set contract instance into a variable hashMarketContract = instance; // Subscribe to a Solidity event instance.ItemAdded({}).watch((error, result) => { if (error) { console.log(error); } // Once the event is triggered, store the result in the // external variable itemID = result.args.itemID; }); // Call the addNewItem function and return the promise return instance.addNewItem(itemName, itemPrice, {from: itemSeller}); }).then(function() { // This function is triggered after the addNewItem call transaction // has been mined. Now call the getItem function with the itemID // we received from the event return hashMarketContract.getItem.call(itemID); }).then(function(result) { // The result of getItem is a tuple, we can deconstruct it // to variables like this var [name, price, seller, status] = result; // Start testing. Use web3.toAscii() to convert the result of // the smart contract from Solidity bytecode to ASCII. After that // use the .replace() to pad the excess bytes from bytes32 assert.equal(itemName, web3.toAscii(name).replace(/\u0000/g, ''), "Name wasn't properly added"); // Use assert.equal() to check all the variables assert.equal(itemPrice, price, "Price wasn't properly added"); assert.equal(itemSeller, seller, "Seller wasn't properly added"); assert.equal(status, 0, "Status wasn't properly added"); }); }); }); 

Запустите truffle test и вы должны получить что-то вроде этого:

 TestHashMarket ✓ testAddingNewProduct (109ms) Contract: HashMarket ✓ should add a new product (64ms) 2 passing (876ms) 

Ваш тест пройден, и вы можете быть уверены в отсутствии ошибок регрессии.

Для домашней работы напишите тесты для всех других функций в контракте.