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

Комментарии (1)


  1. aleshina_mari
    30.09.2023 17:29

    Спасибо, очень полезно