Напишите лучшие смарт-контракты

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

В настоящее время наиболее известными инструментами модульного тестирования для модульного тестирования Solidity являются следующие:

Тестовая среда OpenZeppelin появилась недавно, поэтому она не выглядит достаточно проверенной, хотя выглядит многообещающей. Ремикс - один из самых мощных инструментов редактирования. Но модульное тестирование лучше проводить в режиме командной строки, чем в графическом интерфейсе. Поэтому на данный момент в основном рекомендуется использовать платформу для тестирования смарт-контрактов Truffle.

Ссылка ниже - это программа модульного тестирования Truffle для смарт-контракта ERC-20.

В тестовой программе используется несколько тактик, которых нет в документации Truffle. Они могут сделать программы тестирования более эффективными и действенными, о чем будет сказано ниже. Если вы не знакомы с тестовой средой Truffle, лучше сначала прочитать официальную документацию¹.

[1] Трюфель: Написание тестов на JavaScript

Группировка тестовых случаев

В тестовой среде Truffle используется хорошо известный Mocha¹. Немного изменяя базовую структуру теста Mocha, тестовая программа Truffle начинается с функции contract() и содержит it() функции в качестве тестовых примеров внутри нее².

Если смарт-контракт содержит десятки модульных тестов, такую ​​линейную структуру, подобную приведенной выше, становится трудно читать, поддерживать и обновлять. Mocha позволяет вкладывать тестовые примеры в промежуточные describe() функции, поэтому тестовый пример Truffle может использовать вложенные describe() функции для группировки тестовых примеров для большей читабельности.

В приведенной ниже тестовой программе тестовые примеры сгруппированы в Initial State, Minting, Transfer, Approval, Delegated Transfer, Burning и Circuit Breaker в соответствии с концепциями верхнего уровня токена. Эти группы представляют собой describe() функции, и они содержат it() функций в качестве тестовых примеров внизу.

При большом количестве тестовых примеров тестирование вновь добавленных тестовых примеров может быть очень неэффективным, если все существующие тестовые примеры также выполняются каждый раз. Разделение тестовых примеров на несколько тестовых программ во избежание этого вызовет другие проблемы с согласованностью и ремонтопригодностью. Вы можете использовать функцию only() для запуска только выбранных тестовых случаев, которые вы хотите протестировать с помощью фреймворка Mocha³.

Если приведенная ниже тестовая программа выполняется, будут запущены только тестовые примеры из категории Transfer, из которых функция describe() помечена only().

Вы можете применять only() к it() функциям, чтобы еще больше сузить область выполнения. Несколько describe() или it() функций могут быть помечены only() в тестовой программе.

При выполнении приведенной ниже тестовой программы будут запущены только два тестовых примера, начинающиеся с it.only, все остальные тестовые примеры будут пропущены.

[1] Mocha: многофункциональная среда тестирования JavaScript
[2] Тест Truffle в JavaScript
[3] Mocha / Эксклюзивные тесты

Большое число

Ethereum предпочитает большие числа. Неявная единица в Ethereum - это wei, а наиболее типичный эфир - 10¹⁸ wei¹.

В JavaScript максимальное целочисленное значение для примитивного числового типа составляет около 2⁵³ (~ 10¹⁶) ². Итак, чтобы обрабатывать Ethereum с помощью JavaScript, необходим другой числовой тип для огромных чисел. web3.js³, один из самых фундаментальных гаджетов для Ethereum, использует bn.js⁴ и bignumber.js ⁵. Это показывают типы данных параметра value и параметра gasPrice в функции web3.eth.sendTransaction(). Хотя непонятно, почему web3.js поддерживает два разных типа для огромных чисел, учитывая web3.utils.BN() и web3.utils.toBN(), кажется, что предпочтительнее использовать _33 _ (_ 34_).

Чтобы легче обрабатывать большие числа внутри тестовой программы, определите ссылку на web3.utils.toBN() функцию в начале.

const toBN = web3.utils.toBN;

_36 _ (_ 37_) API содержит различные функции, включая арифметические операции, операции сравнения и побитовые операции. Некоторые функции названы с постфиксом n, что означает, что операнд должен быть примитивного числового типа.

В следующем примере кода функции с обычными именами, такими как add(), div(), sub(), eq(), принимают операнд типа BN, а функции с постфиксом n, такие как addn(), divn(), muln(), eqn(), имеют операнд примитивного числового типа.

[1] Ether
[2] Number.MAX_SAFE_INTEGER
[3] web3.js: Ethereum JavaScript API
[4] bn.js: BigNum в чистом javascript
[5] bignumber.js: библиотека JavaScript для арифметики произвольной точности.

Случайные тестовые данные

Один из самых простых способов увеличить тестовое покрытие и избежать случайного результата теста - это использовать случайные тестовые данные ² ³.

Ниже приведен тестовый пример, чтобы проверить, что имя и символ, указанные в конструкторе, правильно настроены и запрашиваются для контракта токена. Если контракт токена устанавливает поле имени с жестко заданным значением «RGB», хотя это определенно дефект, следующий тестовый пример не может его обнаружить. В тестовом примере случайно используется тот же буквальный «RGB», что и в тестовых данных, поэтому тест не завершится неудачно.

Чтобы предотвратить такой случайный результат, мы можем использовать случайные тестовые данные, как в примере ниже. Chance ⁴ - это библиотека JavaScript для генерации случайных данных в различных форматах и ​​ограничениях. Несколько функций Chance используются для создания случайных предложений и слов или для выбора элемента из массива.

В следующем примере случайная генерация используется для установки суммы в монетном дворе (balance), суммы для перевода (delta) и счетов для отправителя и получателя (sender, recipient). В случае суммы для перевода chance.bool({likelihood: 10}) используется в качестве граничного условия (нулевая сумма), которое нужно попробовать примерно в 10%.

Chance предоставляет более 80 функций для различных типов и форматов, включая number, text, date-time, location и так далее. В каждой функции подробные аспекты или ограничения могут быть установлены с помощью option.

[1] Случайные тестовые данные (MSDN)
[2] Случайное тестирование (Википедия)
[3] Рекомендации по модульному тестированию
[4] Шанс: минималистский генератор случайных строк, чисел и т. д.

Вернуть, Событие

Смарт-контракты асинхронно взаимодействуют с внешними системами, поэтому результат транзакции не может быть доставлен с помощью возвращаемого значения. Вместо этого запускаются события и выдается квитанция о транзакции. Чтобы более тщательно подтвердить, ведет себя смарт-контракт, как ожидалось, тесты смарт-контрактов должны проверять сработавшие события. Тесты смарт-контрактов также должны проверять спроектированные или запланированные откаты в случаях сбоя, таких как недопустимое входное значение, недостаточные привилегии, недостаточный баланс и т. Д.

Для проверки возврата или событий после отправки транзакции требуется квитанция об обработке транзакции¹ ². Код может быть немного подробным, поэтому было бы полезно, если есть вспомогательная функция. К моему удивлению, тестовый фреймворк Truffle его не предоставляет. Но мы можем использовать следующие библиотеки.

В настоящее время две библиотеки предоставляют аналогичные функции. Последнее предпочтительнее из-за значимости имени OpenZeppelin.

Чтобы использовать помощники тестирования OpenZeppelin³ ⁴, необходимо импортировать модуль @openzeppelin/test-helpers.

Чтобы подтвердить, что транзакция была отменена с помощью тестового примера, используйте функцию expectRevert.unspecified().

Чтобы проверить события с помощью тестового примера, используйте функцию theexpectEvent(). Аргументы события, включая имена и значения параметров, могут быть подтверждены.

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

[1] web3.eth.getTransactionReceipt
[2] Углубитесь в журналы Ethereum
[3] Исходный проект OpenZeppelin Test Helpers
[4] Справочник по API OpenZeppelin Test Helpers

ECMAScript 8 (2017)

С момента рождения JavaScript прошло довольно много времени, и он все еще быстро развивается. JavaScript был стандартизирован с помощью ECMAScript, и с 2015 года ежегодно объявляется новая версия спецификации ².

Для более эффективной работы с тестовыми программами Truffle важно выбрать правильную версию JavaScript, которая содержит полезные функции для тестирования.

JavaScript по своей природе асинхронен. Большинство фреймворков и библиотек, включая web3.js и Truffle contract abstraction, работают асинхронно. Программные потоки с асинхронными процессами, обрабатывающими обратные вызовы или обещания, могут быть полезны для производительности, но они могут быть более сложными и трудными. Для тестовой программы удобочитаемость и простота могут быть важнее оптимизации или производительности.

Операторы async ³ / await⁴ разрешают синхронные потоки асинхронных функций, поэтому код может избежать наложения обратного вызова в стек. Оператор await действителен только в блоке async. В случае теста Трюфеля вторым аргументом тестовой функции функции it() будет функция async, а затем вызов await будет выполнен внутри тестовой функции.

В приведенном ниже примере показаны все вызовы контракта токена (Token.new, token.mint, token.totalSupply, token.transfer) await внутри функции async (async() => {}).

it("...", async() => {
  const chance = new Chance();
  const admin = chance.pickone(accounts);
  const token = await Token.new('Color Token', 'RGB', {from: admin});
  let balance = 0;
  for(const acct of accounts){
    balance = toBN(1E19).muln(chance.natural({min:1,max:100}));
    await token.mint(acct, balance, {from: admin});
  } 
  const total = await token.totalSupply();
  
  const loops = 20;
  let sender = 0, recipient = 0, delta = 0;
  for(let i = 0; i < loops; i++){
    sender = chance.pickone(accounts);
    recipient = chance.pickone(accounts);
    delta = toBN(1E13).muln(chance.natural({min:0,max:100}));
    await token.transfer(recipient, delta, {from: sender});
    assert.isTrue((await token.totalSupply()).eq(total));
  }
});

В JavaScript объявление переменной с использованием ключевого слова var имеет необычную семантику, такую ​​как определение области видимости и подъем функции⁵. Это не обычная функция для других языков программирования и может расстроить программистов, использующих неродной JavaScript, даже при использовании простых кодов. ECMAScript 6, представленный в 2015 году, представил операторы const⁶ и let⁷, чтобы компенсировать эти неожиданные эффекты var. Объявление переменной с использованием const и let имеет область видимости блока, а не подъема⁸. Подъем на самом деле может быть более сложным за кулисами⁹. Но практически нет подъема для const и let по сравнению с var. Поэтому настоятельно рекомендуется использовать const и let, если предполагается определение области действия или подъем функций.

Чтобы сделать объявление и использование переменных еще менее подверженным ошибкам, также рекомендуется использовать строгий режим¹⁰. Достаточно добавить литерал 'use strict' в начало строки функции contract(). Эта простая строка удалит многие старые функции, подверженные ошибкам, и заставит вас чувствовать себя более комфортно.

contract("ERC20Regular Contract Test Suite", async accounts => {
  
  "use strict";
  
  if(accounts.length > 8){  // avoid too many accounts
    accounts = (new Chance()).pickset(accounts, 8);
  }
  ...
)};

Операторы _90 _ / _ 91_ были добавлены в ECMAScript 8 и _92 _ / _ 93_ в ECMAScript 6. Node.js 9.11.2 или выше, почти полностью поддерживающий ECMAScript 8, рекомендовал использовать эти операторы.

[1] Версии ECMAScript
[2] Версии JavaScript
[3] async оператор
[4] await оператор
[5] JavaScript Scoping и подъем
[6] const оператор
[7] let оператор
[8] Различия между var и let
[9] Подъем в современном JavaScript - let, const и var
[10] строгий режим
[11] Таблицы совместимости Node.js с ECMAScript

Интерфейс командной строки Ganache

Модульное тестирование может быть довольно утомительным, поэтому необходима тестовая среда с быстрым выполнением. В частности, с Ethereum, среда, в основном идентичная основной сети, насколько это возможно, но в алгоритме консенсуса, отличном от PoW, требуется. Модульные тесты смарт-контрактов в основном не зависят от алгоритма консенсуса. В качестве тестовой среды предпочтительны тестовые сети с PoA, такие как Rinkeby или Kovan, или реализации локального автономного клиента (узла) Ethereum, такие как Ganache или Ganache CLI.

Интерфейс командной строки Ganache разработан давно, работает быстро и предоставляет различные настраиваемые параметры¹, что делает его очень полезным в тестовой среде.

Следующая командная строка запустит экземпляр Ganache CLI, подходящий для модульного тестирования смарт-контрактов.

ganache-cli --networkId 31 \
    --host '127.0.0.1' --port 8545 \
    --gasPrice 2.5E10 --gasLimit 4E8 \
    --deterministic \
    --defaultBalanceEther 10000 --accounts 10 --secure \
    --unlock 0 --unlock 1 --unlock 2 --unlock 3 --unlock 4 \
    --hardfork 'petersburg' \
    --blockTime 0 \
    --db '/var/lib/ganache-cli/data' >> /var/log/ganache.log 2>&1

[1] Параметры интерфейса командной строки Ganache