New to blockchain software development? Read my beginners guide here

Guide to Foundry (and how to write Solidity tests with it)

Created on December 2022 • Tags: solidityethereumguides

A guide to using Foundry and how to write tests for your Solidity contracts with it


Table of Contents for Guide to Foundry (and how to write Solidity tests with it)


How to install and set up Forge

It is best to check out the official guide on the foundry docs. Once you install it return here for a guide on setting the rest of foundry up.

How to set up a new project in Forge

You can start a new empty project with this command:

forge init your_project_name

Quick start guide/Common commands

  • Set up a new project with forge init
  • Build Solidity files with forge build
  • Test your files with forge test

Most of this guide will focus on the tests, as that is the main feature of Forge.

The tests for your Solidity code are also written asSolidity contracts. Forge will call each of the functions in those test files, and if they revert the test fails. Otherwise, it classes the test as passed.

There are other helper functions built in. Here are some of the most common/important helpers:

  • adding support for console.log(...) (you have to run forge test --vv to see the output). Very useful to debug things.

Writing tests in Forge

Your test files use the Forge’s standard library which can be found on https://github.com/foundry-rs/forge-std. You can import it by adding import "forge-std/Test.sol"; at the top of your test files.

Here is a simple example of a test file:

pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../src/Counter.sol";

contract CounterTest is Test {
Counter public counter;

function setUp() public {
counter = new Counter();
counter.setNumber(0);
}

function testIncrement() public {
counter.increment();
assertEq(counter.number(), 1);
}

function testSetNumber(uint256 x) public {
counter.setNumber(x);
assertEq(counter.number(), x);
}
}

setUp function

This is a function you can optionally add and if it is in the contract then it will be run before each test case.

(It is similar to Jest’s beforeEach())

Functions prefixed with test (e.g. testSetNumber())

Functions with a test prefix are the tests that are run automatically. If the function reverts when called, then it is seen as a failed test. Otherwise the test passes.

They must be either public or external. Functions prefixed with test that are internal or private will not be run.

For example:

function testNumberIs42() public {
assertEq(testNumber, 42);
}

There is also testFail prefix - where it will pass if the function call reverts. for example:

function testFailSubtract43() public {
testNumber -= 43;
}

It is also common in Forge tests to see things prefixed with testCannot. There is no specific support for the ‘cannot’ word - but inside these tests you will see some set up to say it expects a revert.

function testCannotSubtract43() public {
vm.expectRevert(stdError.arithmeticError);
testNumber -= 43;
}

Note: the vm.expectRevert is what is known as a cheat code. These will be explained later.

Get traces

Run your tests with -vvv (or -vvvv) to see trace output.

The output from the traces can be read like this:

  [<Gas Usage>] <Contract>::<Function>(<Parameters>)
├─ [<Gas Usage>] <Contract>::<Function>(<Parameters>)
│ └─ ← <Return Value>
└─ ← <Return Value>

They are color coded:

  • Blue: For calls to cheat codes
  • Cyan: For emitted logs
  • Green: For calls that do not revert
  • Red: For reverting calls
  • Yellow: For contract deployments

Forking blockchain with Foundry

Forking allows you to run tests in the context of the mainnet blockchain.

This means you can run scripts locally and they can interact with smart contracts on the mainnet, as if it was really connecting to mainnet. But of course, it is all simulated and just for your test environment.

This can be very useful when testing things out or running audits.

You can pick a specific block to fork to, and change what address msg.sender is set to.

The cheatcode vm.warp(uint256 blockNum) is used to change to that block. And you can use vm.prank(address someAddress) to set msg.sender to that address.

To use the warp feature you need to set up a RPC URL.

Then you can run this:

forge test --fork-url <your-rpc-url> -vv

When you warp it will also set:

  • block_number
  • chain_id
  • gas_limit
  • gas_price
  • block_base_fee_per_gas
  • block_coinbase
  • block_timestamp
  • block_difficulty

For more details see here.

Fuzz testing in Foundry

Fuzz testing is a way to automate some tests, by letting the test runner provide different values for some inputs to a functions.

You often use it to check that the state of the code being tested does not become invalid.

For example, if you were testing a function that takes in two parameters and adds them (function (a) { return a + a; }), you might want to automatically run tests on various values for a, and check they make sense (e.g. not result in a odd number, that the function runs and does not throw an error etc).

With fuzz testing you don’t really give an input and look for a specific output. It is more about checking that something “bad” doesn’t happen.

In Solidity you will often fuzz test and check balances are the same as they were at the start, or increased. It depends on the function you’re testing.

With Foundry it is very simple to fuzz test. Just write a test function, and add a param (e.g. function testSomething(uint256 someValue) ...), and Foundry will run the test function multiple times with different values.

If you write a test like this:

contract SafeTest is Test {
// ...

function testWithdraw(uint256 amount) public {
payable(address(safe)).transfer(amount);
uint256 preBalance = address(this).balance;
safe.withdraw();
uint256 postBalance = address(this).balance;
assertEq(preBalance + amount, postBalance);
}
}

Foundry will see that this test function accepts a uint256 param, so it will try various values for this (such as 0, 2^256, and various numbers in between).

By default it will generate 256 ‘runs’ (each run has a different value) but this is configurable.

Differential fuzz testing

Differential testing is where you check that the outputs of two functions are the same.

For example, in JS if you had const f1 = x => x * 2 and const f2 = x => x + x, you may want to run differential testing to ensure they always have the same output.

Diff fuzzing is like that, but it automatically generates different inputs to try and find differences in the two functions under test.

This is useful if you are upgrading/updating code, or have written a more optimized version but you want to verify it works the same way as the old or slower function.

Diff fuzz testing is easy to do in Foundry. See here for full docs.

Deploying with Forge

You can use the command line forge to deploy your contracts.

Example command to deploy it:

$ forge create --rpc-url <your_rpc_url> --private-key <your_private_key> src/MyContract.sol:MyContract
compiling...
success.
Deployer: 0xa735b3c25f...
Deployed to: 0x4054415432...
Transaction hash: 0x6b4e0ff93a...

You can also use the --verify to automatically verify the contract once it is on the blockchain (this gets the smart contract source code to show up on Etherscan).

Get info about gas usage in your contracts

Tracking the amount of gas your smart contracts consume when run is very important.

You can do this in Foundry. Set the following in your foundry.toml:

gas_reports = ["*"]

(You can also specify an array of specific contracts to generate gas reports for, with gas_reports = ["YourContract", "AnotherContract"])

Then run forge test --gas-report and you will get the full gas report.

Using the Forge debugger

You can use the interactive debugger that comes with the forge CLI tool.

How to use it:

Run a command such as forge test --debug "testSomething()"

When the debugger is running, you can step through your code. here is a guide to using the debugger.

Cheatcodes

Cheatcodes are a concept in Forge that are testing specific helpers.

You’ve seen one in the example above - vm.expectRevert(stdError.arithemeticError). These are helpers you can run in Foundry tests, but of course they would fail to work if deployed to a real blockchain. Its just within the forge tests that it will work.

Cheatcodes are run by calling certain functions on the 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D address.

You can also access this contract with the vm variable (if you import the standard Forge Test contract with import "forge-std/Test.sol";).

Examples:

Set the next call’s msg.sender address with vm.prank(someAddress)

Set the current caller as address 0x000.... with vm.prank(address(0)).

After calling that, msg.sender will appear to come from 0x0000....

If you want to set msg.sender for all subsequent calls (and not just the next one) you can use vm.startPrank(someAddr) and stop it with vm.stopPrank().

(vm.stopPrank() resets it back to address(this).)

You can also set a 2nd param when calling prank or startPrank and it will be used as the tx.origin address, like this:

  • vm.prank(msgSenderAddr, txOriginAddr)
  • or vm.startPrank(msgSenderAddr, txOriginAddr)

Examples:

/// function withdraw() public {
/// require(msg.sender == owner);

vm.prank(owner);
myContract.withdraw(); // [PASS]


Set an account balance (ETH)

Use vm.deal(someAddress, 1000000) to set the address of someAddress to have balance of 1,000,000.

Assert that events were emitted with vm.expectEmit()

Use expect emit to assert that events were emitted.

This works in an unusual way, with 3 main steps:

  • You first call vm.expectEmit() (with some boolean arguments),
  • then you emit an event (of the same type that you are expecting)
  • then you run the actual function you want to test

Foundry will then check that the emitted event (in step 2) matches an event that was emitted in step 3.

The boolean values passed to vm.expectEmit(true, true, true, true) are either true or false, and if they are true then their topic must match the event you emitted (step 2). First topic is the event name btw.

Set block.timestamp with vm.warp(uint256)

If you need to change the block timestamp, call this in your Foundry tests:

vm.warp(1641080800);
emit log_uint(block.timestamp); // 1641080800

Load / Set storage data in a slot, at an address

You can load or set storage data at a specific slot at an address with vm.load() and vm.store()

    // Loads a storage slot from an address
function load(address account, bytes32 slot) external returns (bytes32);

// Stores a value to an address' storage slot
function store(address account, bytes32 slot, bytes32 value) external;

Convert normal Solidity types to strings

Sometimes when running foundry tests you want to see some data represented as a string. This can be useful when debugging. Here are some helper functions in Foundry:

function toString(address) external returns(string memory);
function toString(bytes calldata) external returns(string memory);
function toString(bytes32) external returns(string memory);
function toString(bool) external returns(string memory);
function toString(uint256) external returns(string memory);
function toString(int256) external returns(string memory);

Making assertions in Foundry

Assert booleans in Foundry

You can use assertTrue(val) or assertFalse(val) to check that a value is true or false.

bool someReturnValue = yourFunctionCall();
assertTrue(someReturnValue);

Assert two values are equal

If you want to assert two values (bools, bytes, int256 and uint256 types) are equal, use assertEq(valA, valB).

vm.expectEmit() (cheatcodes)

Explained in more detail above, but you can use vm.expectEmit(...) to check that events were emitted.

You first call expectEmit, passing in some booleans. Each boolean corresponds to the topics of an event that you want to check match an example event that you manually emit straight after calling expectEmit. Then you run your function you are testing, and it will check that the function emits a similar event as your sample one. Its a bit unusual the first few times using it, but it makes sense quite quickly.

Example from docs:

event Transfer(address indexed from, address indexed to, uint256 amount);

function testERC20EmitsTransfer() public {
// Only `from` and `to` are indexed in ERC20's `Transfer` event,
// so we specifically check topics 1 and 2 (topic 0 is always checked by default),
// as well as the data (`amount`).
vm.expectEmit(true, true, false, true);

// We emit the event we expect to see.
emit MyToken.Transfer(address(this), address(1), 10);

// We perform the call.
myToken.transfer(address(1), 10);
}

vm.expectCall() (cheatcodes)

Use this cheatcode to check that a specific function (or calldata) was called on a specific address. Note : this only works for external calls, not internal calls.

Example from Foundry docs:

address alice = address(10);
vm.expectCall( address(token), abi.encodeCall(token.transfer, (alice, 10)));
token.transfer(alice, 10);

vm.expectRevert() (cheatcodes)

After calling vm.expectRevert(), the next call should revert. If it does not, the test fails.

You can pass in a msg param to vm.expectRevert(bytes("some error msg")) to assert that a specific revert message was thrown.

Example

function testACoupleOfReverts() public {
// assertion 1:
vm.expectRevert(abi.encodePacked("NO_AMOUNT"));
vault.send(user, 0);

// assertion 2:
vm.expectRevert(abi.encodePacked("NO_ADDRESS"));
vault.send(address(0), 200);
}

Using Cast

You can use the cast cli tool (included with Foundry) to make RPC calls to the Ethereum blockchain.

It can be used for things like:

  • send transactions (on mainnet & testnet)
  • make contract calls
  • retrieve information from the blockchain
  • and lots of helpers, such as ways to decode calldata

Here is an example command to get the total supply from the DAI smart contract:

cast call 0x6b175474e89094c44da98b954eedeac495271d0f "totalSupply()(uint256)" --rpc-url https://eth-mainnet.alchemyapi.io/v2/<your-token-here>
  • 0x6b175474e89094c44da98b954eedeac495271d0f is the address of the DAI contract
  • "totalSupply()(uint256)" is the contract function you want to call
  • and lastly the --rpc-url is the RPC provider, in this case Alchemy. You can also set these via config so you don’t have to put the url every time

Example of using cast to decode calldata. This will decode it via ABI-encoded calldata from https://sig.eth.samczsun.com. In the next example it shows the 1 match that 1F1F897F676d could be - a function called fulfillRandomness.

$ cast 4byte-decode 0x1F1F897F676d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003e7
1) "fulfillRandomness(bytes32,uint256)"
0x676d000000000000000000000000000000000000000000000000000000000000
999

Here are some more commands you can use with Cast.

  • cast-publish will publish a raw transaction to the network (link)
  • cast-send will sign and publish a transaction (link)
  • cast gas-price will get the current gas price. (link)
  • cast balance gets the balance of an account in wei (link)
  • cast keccak hashes data with kekkak256 (link)
  • and many more not listed here

It is a very useful command to have in your toolkit when working with EVM blockchains.

Full documentation for cast can be found on [https://book.getfoundry.sh/reference/cast/cast](the official site)

Using Anvil

You can run test files in Foundry without spinning up (or connecting to) an actual Ethereum blockchain. But sometimes you will want to test things in a frontend, or have another client interact with your smart contracts over RPC.

For that you can use Anvil to spin up a testnet.

Anvil is included when you install Foundry, and is easy to get set up.

#  Number of dev accounts to generate and configure. [default: 10]
anvil -a, --accounts <ACCOUNTS>

# The EVM hardfork to use. [default: latest]
anvil --hardfork <HARDFORK>

# Port number to listen on. [default: 8545]
anvil -p, --port <PORT>

# Produces a new block every 10 seconds
anvil --block-time 10

# Enables never mining mode
anvil --no-mining

# Set the number of accounts to 15 and their balance to 300 ETH
anvil --accounts 15 --balance 300

# Choose the address which will execute the tests
anvil --sender 0xC8479C45EE87E0B437c09d3b8FE8ED14ccDa825E

# Change how transactions are sorted in the mempool to FIFO
anvil --order fifo

For full documentation please see here

Configuring Foundry

The most common way to set config for Foundry is with foundry.toml

Forge can be configured using a configuration file called foundry.toml, which is placed in the root of your project.

Configuration can be namespaced by profiles. The default profile is named default, from which all other profiles inherit. You are free to customize the default profile, and add as many new profiles as you need.

Additionally, you can create a global foundry.toml in your home directory.

Example:

[profile.default]
optimizer = true
optimizer_runs = 20_000

[profile.ci]
verbosity = 4

Using Foundry in CI tests (like Github actions)

You can easily set up Foundry to run tests on your Solidity code as part of CI/CD.

Here is an example Github actions workflow to run Foundry tests on every push to a github repo.

on: [push]

name: test

jobs:
check:
name: CryptoGuide.dev Foundry Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: recursive

- name: Installation
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly

- name: Run foundry tests
run: forge test -vvv

Static analysis with Slither and Mythril

It is possible to use Slither to run static analysis in a Foundry project.

For more see slither.

You can also do it for mythril. Instructions can be found on the official Foundry docs site.

Using Hardhat with Foundry

You can easily use Hardhat with Foundry.

Hardhat uses Javascript to write tests for your Solidity contracts, but Foundry uses Solidity contracts to test your Solidity contracts.

While I am a big fan of foundry, there are times when you will be wishing you could use HH too. Its quite easy to get it set up.

this tends to be the best guide I’ve found.

Other notes

  • Use the .t.sol suffix for your test files. For example, to test MyToken.sol, the common naming convention for test files for foundry is MyToken.t.sol.
  • If your test file is huge, its common to see test files split up, so you might have MyToken.transfers.t.sol, MyToken.owner.t.sol etc.
  • Don’t put assertions in your setUp function. If they fail, it will not fail the test.
  • Its a common pattern in Foundry tests to organize your test files in the same order as your contract (so the functions at the start (top) of your contract are also the first tested). This can make auditing and reading the tests much easier.
  • You can test internal functions with what is called a test harness. The harness contract is one that extends the contract you’re testing (“Contract under test” or “CuT”), and exposes the internal function that you want to test. The common naming pattern for these is exposed_yourInternalFuntion().
    • Private functions cannot be easily tested in this way. You can either copy/paste them, or make them internal.

Common issues with Forge

How to see console.log output in Forge?

  • Make sure you import import "forge-std/console.sol"; at the top of your Solidity files
  • Then add console.log("something"), or things such as console.logInt(1234);
  • But if you run forge test you won’t see any output. To see logs when running forge test you have to:
    • run the test command in verbose mode (forge test --vv)

You can also check out this guide on debugging console log output in forge

Running tests fail, and how to debug it

Sometimes it can be hard to figure out why a Forge test is failing. A solution I often resort to is to run it with more verbosity. forge test --vvv or even forge test --vvvv.

Difference between Foundry and Forge

  • Foundry is the main test runner/development environment.
  • Forge is the CLI tool (included in foundry)

This post is incomplete and a work-in-progress
I'll update it soon and flesh it out with more info!

Spotted a typo or have a suggestion to make this crypto dev article better? Please let me know!

See all posts (70+ more)

See all posts (70+ more)

Was this post helpful? 📧

If you liked this content and want to receive emails about future posts like this, enter your email. I'll never spam you.

Or follow me on @CryptoGuide_Dev on twitter

By using this site, you agree that you have read and understand its Privacy Policy and Terms of Use.
Use any information on this site at your own risk, I take no responsibility for the accuracy of safety of the information on this site.