Testing smart contracts in Foundry (part 1)

Foundry is a fairly fresh and very powerful tool for developing, deploying and testing smart contracts in the Solidity language, and recently it has been gaining wild popularity. The main reason, in my opinion, is that Foundry allows you to implement all stages of project development in one language without knowledge of JavaScript, Node.js, etc.

At the moment, I could find very little information on this tool in the Russian-language media field, so I decided to make a small methodological recommendation for those who are very poor at English and who want to quickly get involved and learn how to work with this framework.

In this part we will install, create and configure the project, learn how to test errors, events and equalities.

I want to immediately warn you that Foundry contains a ton of different commands, scripts, additional plugins and tools. In this course, I will touch on the basic commands and limit myself to explaining their work at the black box level. If you’re interested in learning how some commands are implemented, you can check out open source or documentation

Source code for this part of the course

Introduction to Foundry (Forge, Anvil)

Before you start working with Foundry, you should install it. For calm and comfortable work, I recommend using the UNIX family OS (WSL if you have Windows). Installation is as simple and straightforward as possible, described in detail on the documentation site here

To work, I advise you to use VS code with with this plugin

Creating a new project

After Foundry has been successfully installed, you can create your first project

The following command is responsible for initializing the project:

$ forge init

Something like this structure should appear in the directory in which you ran this command

By default, the script, src and test directories will contain test files to demonstrate how the framework works.

Briefly about directories and files:

  • lib — stores all the dependencies that are needed for the project, but not in the same way as node_modules (node.js), so this folder can be safely pushed into Git and not added to .gitignore

  • src — the main smart contracts of your project with which you directly work will be stored here

  • script — scripts for working with contracts will be written here. These can be scripts for deploying a specific contract, calling a specific function, etc. Files in this directory must be in .s.sol format

  • test – similarly for tests, files must be in .t.sol format

  • foundry.toml – this is a project configuration file; you will need it more than once to set up tests, dependencies, and other things.

First contract, first test.

I’ll work with the Counter.sol contract, but I’ll just modify it a little:

// 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();
    }
}

If you are working in VS code with the plugin enabled, you will most likely see this error

Of course we need to install OpenZeppelin contracts to use them. To do this we will use the following command:

$ forge install OpenZeppelin/openzeppelin-contracts

If you encounter an error during the installation process, try adding the key “ – no-commit

Now the corresponding directory should appear in the lib folder, but the problem is not completely resolved. The thing is that in order for our import in the contract to work, we must direct it to our folder lib/openzeppelin-contracts.. To do this, we will do the appropriate remapping (add the following code in the foundry.toml file)

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

After this, the error should disappear and now we can compile our project

$ forge build

Now let’s look at the test for this contract (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();
    }
}

If you have a question, what is selector or you don’t understand some lines of code, I recommend taking a look a series of training videos from Ilya Krukovsky or read a very interesting book from one of the creators of Solidity – “Mastering Ethereum”

Selector — these are the first 4 bytes of the hash of the function signature (in our case, the error object)

To put it simply, selector – this is the object identifier by which the Solidity compiler understands which function or error to call

To check the tests we will use the command:

$ forge test

If you did everything correctly, then pleasant and calming green logs should appear in the console 🙂

If you wish, you can specify which test you want to run using the “-match-test” key, for example:

$ forge test --match-test testIncrement

Conclusion

In the first (introductory) part, we touched on the most basic Foundry testing tools, tested simple functions, errors (in the form of text and selectors), events, and touched a little on fuzz testing.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *