Перейти к основному содержанию

Как имитировать умные контракты Solidity для тестирования

Solidity
Умные контракты
тестирование
имитация
Intermediate
Markus Waas
2 мая 2020 г.
3 минута прочтения

Мок-объекты (opens in a new tab) — это распространенный шаблон проектирования в объектно-ориентированном программировании. Произойдя от старого Французского слова 'mocquer' со значением 'высмеивать', оно эволюционировало до 'имитировать что-то реальное', что актуально, когда мы собираемся программировать. Пожалуйста, чтобы получать удовольствие от Ваших смарт-контрактов, если Вы хотите, то имитируете их всякий раз, когда можете. Это делает Вашу жизнь легче.

Юнит-тестирование контрактов с помощью мок-объектов

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

Можно каждый раз писать сложную логику настройки теста, которая приводит контракт в требуемое состояние, или же написать мок-объект. Имитировать контракт легко с помощью наследования. Просто создайте второй мок-контракт, который наследует от исходного. Теперь вы можете переопределить функции в своем мок-объекте. Давайте посмотрим это на примере.

Пример: закрытый ERC20

Мы используем пример контракта ERC-20, у которого есть начальный закрытый период. Владелец может управлять закрытыми пользователями, и только им будет разрешено получать токены вначале. По истечении определенного времени все смогут использовать токены. Если вам интересно, мы используем хук _beforeTokenTransfer (opens in a new tab) из новых контрактов OpenZeppelin v3.

1pragma solidity ^0.6.0;
2
3import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
4import "@openzeppelin/contracts/access/Ownable.sol";
5
6contract PrivateERC20 is ERC20, Ownable {
7 mapping (address => bool) public isPrivateUser;
8 uint256 private publicAfterTime;
9
10 constructor(uint256 privateERC20timeInSec) ERC20("PrivateERC20", "PRIV") public {
11 publicAfterTime = now + privateERC20timeInSec;
12 }
13
14 function addUser(address user) external onlyOwner {
15 isPrivateUser[user] = true;
16 }
17
18 function isPublic() public view returns (bool) {
19 return now >= publicAfterTime;
20 }
21
22 function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual override {
23 super._beforeTokenTransfer(from, to, amount);
24
25 require(_validRecipient(to), "PrivateERC20: invalid recipient");
26 }
27
28 function _validRecipient(address to) private view returns (bool) {
29 if (isPublic()) {
30 return true;
31 }
32
33 return isPrivateUser[to];
34 }
35}
Показать все

А теперь давайте мокнем его.

1pragma solidity ^0.6.0;
2import "../PrivateERC20.sol";
3
4contract PrivateERC20Mock is PrivateERC20 {
5 bool isPublicConfig;
6
7 constructor() public PrivateERC20(0) {}
8
9 function setIsPublic(bool isPublic) external {
10 isPublicConfig = isPublic;
11 }
12
13 function isPublic() public view returns (bool) {
14 return isPublicConfig;
15 }
16}
Показать все

Вы получите одно из следующих сообщений об ошибке:

  • PrivateERC20Mock.sol: Ошибка типа: У переопределяющей функции отсутствует спецификатор "override".
  • PrivateERC20.sol: Ошибка типа: Попытка переопределить невиртуальную функцию. Вы забыли добавить "virtual"?

Поскольку мы используем новую версию Solidity 0.6, мы должны добавить ключевое слово virtual для функций, которые можно переопределить, и override для переопределяющей функции. Давайте добавим их в обе функции isPublic.

Теперь в своих юнит-тестах вы можете использовать вместо этого PrivateERC20Mock. Когда вы хотите протестировать поведение во время закрытого периода использования, используйте setIsPublic(false) и аналогично setIsPublic(true) для тестирования открытого периода использования. Конечно, в нашем примере мы могли бы просто использовать вспомогательные функции времени (opens in a new tab), чтобы соответствующим образом изменить время. Но идея имитации теперь должна быть ясна, и вы можете представить сценарии, в которых это не так просто, как простое продвижение времени.

Имитация многих контрактов

Может возникнуть беспорядок, если вам придется создавать еще один контракт для каждого отдельного мок-объекта. Если вас это беспокоит, вы можете взглянуть на библиотеку MockContract (opens in a new tab). Она позволяет переопределять и изменять поведение контрактов «на лету». Однако она работает только для имитации вызовов другого контракта, поэтому для нашего примера она не подойдет.

Имитация может быть еще более мощной

Возможности имитации на этом не заканчиваются.

  • Добавление функций: полезно не только переопределять определенную функцию, но и просто добавлять дополнительные функции. Хорошим примером для токенов является наличие дополнительной функции mint, позволяющей любому пользователю бесплатно получать новые токены.
  • Использование в тестовых сетях: когда вы развертываете и тестируете свои контракты в тестовых сетях вместе со своим децентрализованным приложением, рассмотрите возможность использования мок-версии. Избегайте переопределения функций, если в этом нет крайней необходимости. В конце концов, вы хотите протестировать реальную логику. Но может быть полезно добавить, например, функцию сброса, которая просто сбрасывает состояние контракта к начальному, не требуя нового развертывания. Очевидно, что вы бы не хотели иметь это в контракте в основной сети.

Последнее обновление страницы: 25 августа 2025 г.

Было ли это руководство полезным?