Getting Started with Forge

Forge is a fast and flexible testing framework, inspired by Dapp. Part of the Foundry toolkit, a blazing fast, portable and modular kit for Ethereum development written in Rust.

Why should you use it?

  • It’s really fast, no more wasted time while running tests.
  • Allows you to write tests in solidity which minimizes context switching.
  • A lot of testing features like fuzzing, console.log, and cheat codes give you more power and flexibility.
  • Non-standard directory structures support (works fine with Dapp or Hardhat repos).
  • Debugger and many more!

Installing Forge

As foundry is evolving fast and has a lot of contributors, the recommended way to install is using foundryup tool.

Installing Foundryup

On Linux and macOS systems can be installed like:

$ curl -L https://foundry.paradigm.xyz | bash

This command downloads foundryup. Then you can install foundry by running:

$ foundryup

In Windows, you need to install it by building from the source.

Building from source

First, you need to install rust, you can follow the official documentation here:

Then you can use cargo (the Rust package manager) to install foundry:

$ cargo install --git https://github.com/gakonst/foundry --bins --locked

As foundry is evolving fast with a lot of contributors it’s recommended to use forgeup for managing various versions of Forge.

Initialize the Repository

Setup the folder that you want to use and initialize forge:

$ mkdir my-app
$ cd my-app
$ forge init

This creates a simple project structure, you can of course configure it according to your needs, but for simplicity in this tutorial, I will use the default structure.

lib

The libraries that you install will go here, by default, ds-test is installed, this one allows you to run tests, create asserts or logs. If you want to install other libraries you can run:

$ forge install openzeppelin/openzeppelin-contracts

This will add the openZeppelin repo to lib/openzeppelin-contracts and you can use it with a simple import:

import "openzeppelin-contracts/contracts/access/Ownable.sol";

The path for the libs can be configured with remappings. See Appendix B — Using Remappings for further information.

src

The src folder is where your contracts are. We are going to use this simple example to run a few tests:

pragma solidity 0.8.10;

contract Blacksmith {
  address public owner;

  constructor(address _owner){
    owner = _owner;
  }

  function onlyOwner() public{
    require(msg.sender == owner, "Not the owner");
  }
}

test

This is where your test lives, as a best practice, the test file is named exactly as the main contract with an appended “.t”, if your contract is named blacksmith.sol the test should be named blacksmith.t.sol.

We can define where the src and test folders are using environment variables. See Appendix A — Additional Resources for further information.

How to Test

What makes a .sol file a test is inheriting from the DSTest library, which provides a complete list of assertions, equality checks, logs, and helpers that you can use. See the complete list on:

In order to run a test with forge you only need to use the command forge test . Use the following code and try it out:

pragma solidity 0.8.10;

import "ds-test/test.sol";
import "../Blacksmith.sol";

contract User {
  function callOnlyOwner(Blacksmith b) public {
    b.onlyOwner();
  }
}

contract BlacksmithTest is DSTest {
    Blacksmith blacksmith;
    User user;

    function setUp() public {
      blacksmith = new Blacksmith(address(this));
      user = new User();    
    }

    function testOwner() public {
        assertEq(blacksmith.owner(),address(this));
    }

    function testFailOwner() public {
       user.callOnlyOwner(blacksmith);
    }

    function testWrongOwner() public {
        user.callOnlyOwner(blacksmith); // This test will fail
    }
}

Forge and Dapp use certain keywords in functions to run tests: setUp, test, and testFail.

setUp()

This function is called before each test or testFail function, perfect for initialization of contracts and additional setup.

test*()

Functions that start with the keyword test will get run with forge and expect to not revert in order to pass.

testFail*()

Functions that start with the keyword test will get run with forge and expect to revert in order to pass.

Forge implements some cheat codes that make failing tests in a better way, we will cover the cheat codes in another tutorial.


There is so much to cover with forge, but want to keep these tutorials short, will keep a small list of resources and tutorials updated in Appendix A — Additional Resources. Feel free to share this article or contact me by sending a DM on Twitter @Crisgarner.

Appendix A — Additional Resources

Appendix B — Using Remappings

Remappings help when you want to use NPM style imports, or have a hardhat repo that installs their packages in node_modules, for example:

import "@openzeppelin/contracts/access/Ownable.sol";

In order for Forge to know where to find this dependency you need to create a remappings.txt file at the top level of your project directory. So the steps you need to follow are:

  1. forge install openzeppelin/openzeppelin-contracts (this will add the repo to lib/openzepplin-contracts)
  2. Create a remappings file: touch remappings.txt
  3. Add this line to remappings.txt
@openzeppelin/=lib/openzeppelin-contracts/
Subscribe to crisgarner.eth
Receive the latest updates directly to your inbox.
Verification
This entry has been permanently stored onchain and signed by its creator.