Foundry — это довольно свежий и очень мощный инструмент для разработки, деплоя и тестирования смарт-контрактов на языке Solidity, и в последнее время он набирает бешенную популярность. Основная причина, по моему мнению, заключается в том, что Foundry позволяет реализовывать все этапы развития проекта на одном языке без знаний JavaScript, Node.js и прочего.
На данный момент в русскоязычном медиа-поле я смог найти очень мало инфы по данному инструменту, поэтому решил составить небольшую методическую рекомендацию для тех, у кого очень плохо с английским языком и кто хочет быстро втянуться и научиться работать с данным фреймворком.
В данной части мы установим, создадим и настроим проект, узнаем, как тестировать ошибки, ивенты и равенства.
Хочу сразу предупредить, что Foundry содержит в себе тонну различных команд, скриптов, дополнительных плагинов и инструментов. В рамках данного курса я затрону базовые команды и ограничусь объяснением их работы на уровне «чёрного ящика». Если вам интересно узнать, как реализованы некоторые команды, можете изучить открытый исходный код или документацию
Исходный код к данной части курса
Ссылка на мой аккаунт в Хабр Карьере (буду рад знакомству)
Знакомство с 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-тестирование.
aleshina_mari
Спасибо, очень полезно