
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-тестирование.
