Writing Tests

Tests are written in Solidity. If the test function reverts, the test fails, otherwise it passes.

Let’s go over the most common way of writing tests, using the Forge Standard Library’s Test contract, which is the preferred way of writing tests with Forge.

In this section, we’ll go over the basics using the functions from the Forge Std’s Test contract, which is itself a superset of DSTest. You will learn how to use more advanced stuff from the Forge Standard Library soon.

DSTest provides basic logging and assertion functionality. To get access to the functions, import forge-std/Test.sol and inherit from Test in your test contract:

import {Test} from "forge-std/Test.sol";

Let’s examine a basic test:

pragma solidity 0.8.10;

import {Test} from "forge-std/Test.sol";

contract ContractBTest is Test {
    uint256 testNumber;

    function setUp() public {
        testNumber = 42;
    }

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

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

Forge uses the following keywords in tests:

  • setUp: An optional function invoked before each test case is run.
    function setUp() public {
        testNumber = 42;
    }
  • test: Functions prefixed with test are run as a test case.
    function test_NumberIs42() public {
        assertEq(testNumber, 42);
    }
  • testFail: The inverse of the test prefix - if the function does not revert, the test fails.
    function testFail_Subtract43() public {
        testNumber -= 43;
    }

A good practice is to use the pattern test_Revert[If|When]_Condition in combination with the expectRevert cheatcode (cheatcodes are explained in greater detail in the following section). Also, other testing practices can be found in the Tutorials section.

Note: To use stdError constants (like arithmeticError in the example below), make sure to import StdError.sol:

import {stdError} from "forge-std/StdError.sol";

Now, instead of using testFail, you know exactly what reverted and with which error:

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

Tests are deployed to 0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84. If you deploy a contract within your test, then 0xb4c...7e84 will be its deployer. If the contract deployed within a test gives special permissions to its deployer, such as Ownable.sol’s onlyOwner modifier, then the test contract 0xb4c...7e84 will have those permissions.

⚠️ Note

Test functions must have either external or public visibility. Functions declared as internal or private won’t be picked up by Forge, even if they are prefixed with test.

Before test setups

Unit and fuzz tests are stateless and are executed as single transactions, meaning that the state modified by a test won’t be available for a different one (instead, they’ll use the same state created by setUp call). It is possible to simulate multiple transactions in a single test, with a dependency tree, by implementing the beforeTestSetup function.

  • beforeTestSetup: Optional function that configures a set of transactions to be executed before test.
function beforeTestSetup(
    bytes4 testSelector
) public returns (bytes[] memory beforeTestCalldata)

where

  • bytes4 testSelector is the selector of the test for which transactions are applied
  • bytes[] memory beforeTestCalldata is an array of arbitrary calldata applied before test execution

đź’ˇ Tip

This setup can be used for chaining tests or for scenarios when a test needs certain transactions committed before test run (e.g. when using selfdestruct). The test fails if any of the configured transaction reverts.

For example, in contract below, testC is configured to use state modified by testA and setB(uint256) functions:

contract ContractTest is Test {
    uint256 a;
    uint256 b;

    function beforeTestSetup(
        bytes4 testSelector
    ) public pure returns (bytes[] memory beforeTestCalldata) {
        if (testSelector == this.testC.selector) {
            beforeTestCalldata = new bytes[](2);
            beforeTestCalldata[0] = abi.encodePacked(this.testA.selector);
            beforeTestCalldata[1] = abi.encodeWithSignature("setB(uint256)", 1);
        }
    }

    function testA() public {
        require(a == 0);
        a += 1;
    }

    function setB(uint256 value) public {
        b = value;
    }

    function testC() public {
        assertEq(a, 1);
        assertEq(b, 1);
    }
}

Shared setups

It is possible to use shared setups by creating helper abstract contracts and inheriting them in your test contracts:

abstract contract HelperContract {
    address constant IMPORTANT_ADDRESS = 0x543d...;
    SomeContract someContract;
    constructor() {...}
}

contract MyContractTest is Test, HelperContract {
    function setUp() public {
        someContract = new SomeContract(0, IMPORTANT_ADDRESS);
        ...
    }
}

contract MyOtherContractTest is Test, HelperContract {
    function setUp() public {
        someContract = new SomeContract(1000, IMPORTANT_ADDRESS);
        ...
    }
}

đź’ˇ Tip

Use the getCode cheatcode to deploy contracts with incompatible Solidity versions.