Тестирование смарт-контрактов в Foundry (часть 1)

от автора

Foundry — это довольно свежий и очень мощный инструмент для разработки, деплоя и тестирования смарт-контрактов на языке Solidity, и в последнее время он набирает бешенную популярность. Основная причина, по моему мнению, заключается в том, что Foundry позволяет реализовывать все этапы развития проекта на одном языке без знаний JavaScript, Node.js и прочего.

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

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

Хочу сразу предупредить, что Foundry содержит в себе тонну различных команд, скриптов, дополнительных плагинов и инструментов. В рамках данного курса я затрону базовые команды и ограничусь объяснением их работы на уровне «чёрного ящика». Если вам интересно узнать, как реализованы некоторые команды, можете изучить открытый исходный код или документацию

Исходный код к данной части курса

Ссылка на мой аккаунт в Хабр Карьере (буду рад знакомству)

Оглавление

  • Часть 1 (Создание проекта, обзор файловой структуры, assertEq, prank, expectEmit, expectRevert)

  • Часть 2 (Aвтоматическое форматирование кода (forge fmt) отслеживание качества тестирования контрактов (forge coverage), логи (vvvv), управление временем (warp) и деньгами(deal, hoax).)

  • Часть 3 (Работа с прокси-контрактами (Proxy Upgradable Contracts, UUPS), написание скриптов для деплоя и вызова функций, работа с env (переменные среды), автоматизация запусков скриптов (Makefile), форк-тестирование)

  • Часть 4 (fuzz-тестирование, работа с cast)

Знакомство с Foundry (Forge, Anvil)

Перед тем, как преступить к работе с Foundry, следует его установить. Для спокойной и комфортной работы рекомендую использовать ОС семейства UNIX (WSL в случае, если у вас Windows). Установка максимально простая и понятная, подробно изложена сайте документации вот здесь

Для работы советую использовать VS code с данным плагином

Создание нового проекта

После того, как Foundry был успешно установлен, можно создавать первый проект

За инициализацию проекта отвечает следующая команда:

$ forge init

Примерно такая структура должна появится в той директории, в которой вы запустили данную команду

По умолчанию в каталогах script, src и test будут находится тестовые файлы для демонстрации работы фреймворка.

Кратко по каталогам и файлам:

  • lib — хранит все зависимости, которые нужны для проекта, но не так, как node_modules (node.js), поэтому данную папку можно смело пушить в гит и не добавлять в .gitignore

  • src — здесь будут хранится основные смарт-контракты вашего проекта, с которыми вы непосредственно работаете

  • script — здесь будут писаться скрипты для работы с контрактами. Это могут быть скрипты на деплой конкретного контракта, на вызов конкретной функции и т.д. Файлы в данном каталоге должны иметь формат .s.sol

  • test — аналогично для тестов, файлы должны иметь формат .t.sol

  • foundry.toml — это файл конфигурации проекта, он не раз вам понадобится для настройки тестов, зависимостей и прочего

Первый контракт, первый тест.

Я буду работать с контрактом Counter.sol, но только немного его модифицирую:

// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13;  import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";  contract Counter is Ownable {     uint256 public number;      event Incremented();      error onlyNotZero();      constructor() Ownable() {        }      /**      * Функция для установления нового значения number      * Доступна для вызова только owner'y      * @param newNumber не должен быть равен 0      */     function setNumber(uint256 newNumber) public onlyOwner {         if (newNumber == 0) {             revert onlyNotZero();         }         number = newNumber;     }      /**      * Увеличивает значение number на 1      * Инициирует событие Incremented      */     function increment() public {         number++;         emit Incremented();     } }

Если вы работаете в VS code с включенным плагином, то скорее всего вы увидите данную ошибку

Конечно же нам нужно установить контракты от OpenZeppelin, чтобы ими воспользоваться. Для этого воспользуемся следующей командой:

$ forge install OpenZeppelin/openzeppelin-contracts

Если вы столкнулись с ошибкой в процессе установки, попробуйте добавить в конец команды ключ “ — no-commit

Теперь в папке lib должна появится соответствующая директория, но проблема не решена до конца. Всё дело в том, что для того, чтобы наш импорт в контракте работал, мы должны направить его в нашу папку lib/openzeppelin-contracts.. для этого сделаем соответствующий remapping (в файле foundry.toml добавьте следующий код)

remappings = ["@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/"]

После этого ошибка должна пропасть и теперь мы можем скомпилировать наш проект

$ forge build

Теперь давайте рассмотрим тест для данного контракта (Counter.t.sol):

// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13;  //Здесь мы импортируем самый важный контракт для тестирования в Foundry //Его обязательно нужно добавить в наследование к нашему контракту  //для тестирования import {Test} from "forge-std/Test.sol";  import {Counter} from "../src/Counter.sol";  /**  * Структура теста такая же, как и у обычного контракта  * Мы создаём самый обычный контракт, только наследуем его от Test  */ contract CounterTest is Test {     Counter public counter;      //Для дальнейшего тестирования события Incremented     //копируем его в тестирующий контракт     event Incremented(uint256 indexed number);      //Первая функция для тестирования - создание адреса (makeAddr)     //Данная функция принимает в аргументы некоторую строку     //Которая служит, как источник энтропии для генерации адреса     //Обычно, данный параметр называют также, как и переменную     address owner = makeAddr("owner");      /**      * Стартовая функция setUp()      * Она запускается в самом начале выполнения теста (аналог конструктора)      * Изменения состояния, которые происходят в данной функции,      * будут применены ко всем остальным функциям.      * В данном тесте мы используем данную функцию для того,      * чтобы инициализировать тестируемый контракт      */     function setUp() public {         counter = new Counter();         //В данный момент owner контракта - это контракт тестирования         //Для дальнейшей работы следует заменить его на созданный         //ранее адрес (owner)         counter.transferOwnership(owner);     }      /**      * Стандартная тестовая функция      * Её название не важно, изменения, которые в ней происходят      * никак не повлияют на общее состояние, то есть все тесты      * работают в "вакууме"      */     function testIncrement() public {         counter.increment();          //Самая простая и тревиальная функция сравнения двух значений         assertEq(counter.number(), 1);     }      /**      * В примере по-умолчанию уже используется так называемое fuzz-тестирование      * Это более сложный уровень, но на данном этапе погружения можно      * понять это следующим образом: fuzz-тестирование в Foundry используется      * для тестирования нетривиальных (случайных) ситуаций      *      * В данном примере мы поместили в параметры тестирующей функции      * параметр x - это значит, что при тестировании будет сгенерировано      * множество различных (случайных) чисел x      * и с каждым из них будет проведено тестирование данной функции      */     function testSetNumber(uint256 x) public {         if (x != 0) {             //Очень важная и крутая функция startPrank используется для того,             //чтобы следующий участок кода выполнялся             //от имени заданного нам адреса             //В данном примере мы хотим использовать owner, чтобы от его имени             //вызвать функцию setNumber()             vm.startPrank(owner);             counter.setNumber(x);             vm.stopPrank();             assertEq(counter.number(), x);         }     }      /**      * В данной функции мы будет использовать метод для проверки появления      * нужной ошибки - expectRevert()      * Данный метод может не принимать никаких аргументов и будет срабатывать      * при любой ошибке      * Но чтобы сделать наше тестирование более проработанным,      * в качестве аргумента можно добавить текст ошибки      */     function testRevertIfCallerIsNotOwner() public {         vm.expectRevert("Ownable: caller is not the owner");         counter.setNumber(100);     }      /**      * В случае, если контракт использует в качестве вызова      * ошибок специальные объекты error, то в данном случае для      * тестирования данной ошибки      * в качестве аргумента к expectRevert() следует добавить      * селектор нужной нам ошибки Counter.ZeroNumber.selector      */     function testRevertIfNumberIsZero() public {         vm.expectRevert(Counter.ZeroNumber.selector);         vm.startPrank(owner);         counter.setNumber(0);         vm.stopPrank();     }      /**      * В данной функции мы рассмотрим метод для тестирования ивентов -      * expectEmit()      * На первый взгляд он мужет напугать, потому что имеет много      * различных параметров, но на самом деле всё очень просто      * Чтобы протестировать событие, нам нужно:      * 1) Задать данные, которые будет проверять      * 2) "Фиктивно" инициировать событие, которое мы собираемся проверять      * 3) Вызвать функцию, в которой вызывается данное событие      */     function testEmitEventIncremented() public {         //Это одна из простых форм вызова метода expectEmit,         //где в качестве аргумента указывается только адрес того         //от кого ожидаем получения события         vm.expectEmit(address(counter));         emit Incremented(1);         counter.increment();     } }

Если у вас возник вопрос, что такое селектор или вам непонятны некоторые строчки кода рекомендую посмотреть серию обучающих роликов от Ильи Круковского или почитать очень интересную книгу от одного из создателей Solidity — “Осваиваем Ethereum”

Селектор — это первые 4 байта хеша сигнатуры функции (в нашем случае — объекта ошибки)

Если по-простому, селектор — это идентификатор объекта, по которому компилятор Solidity понимает, какую именно функцию или ошибку вызывать

Для проверки тестов воспользуемся командой:

$ forge test

Если вы всё сделали правильно, то в консоли должны появится приятные и успокаивающие зелёные логи 🙂

При желании вы можете конкретизировать, какой именно тест вы хотите запустить c помощью ключа “ — match-test”, например:

$ forge test --match-test testIncrement

Заключение

В первой (вводной части) мы затронули самые базовые инструменты тестирования Foundry, протестировали простые функции, ошибки (в виде текста и селектора), события, и немного затронули fuzz-тестирование.

2 часть >>>


ссылка на оригинал статьи https://habr.com/ru/articles/764576/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *