๐ŸงชTests

Useful Commands
forge test --summary
forge test -vvv
forge test --match-test <SPECIFIC_TEST_FUNCTION_NAME>
forge test --match-contract <SPECIFIC_CONTRACT_NAME>
forge test --debug <SPECIFIC_TEST_FUNCTION_NAME> # h to show/hide help

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

TestDescription

Unit

Testing a specific part of our code

Integration

Testing how our code works with other parts of our code

Forked

Testing our code on a simulated real environment

Staging

Testing our code in a real environment that is not prod

Fuzz

Testing our code against unexpected/random inputs to find bugs

Writing Tests

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 "forge-std/Test.sol";
Example Test Contract
pragma solidity 0.8.10;

import "forge-std/Test.sol";

contract ContractBTest is Test {
    uint256 testNumber;
    string string1;
    string string2;

    // An optional function invoked before each test case is run.
    function setUp() public {
        testNumber = 42;
        string1 = ABC;
        string2 = XYZ;
    }

    // Functions prefixed with test are run as a test case.
    function test_NumberIs42() public {
        assertEq(testNumber, 42);
    }

    // Inverse test prefix - if the function does not revert, the test fails.
    function testFail_Subtract43() public {
        testNumber -= 43;
    }
    
    function test_CompareStrings() public {
        assertEq(
            keccak256(abi.encodePacked(string1)),
            keccak256(abi.encodePacked(string2))
        );
    }
}

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.

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.

Naming Convention

Shared test setups

  • If there are multiple tests that all have the same initial setup configuration, use a helper contract to reduce code duplication

abstract contract TestHelperContract is Test, {
    address constant IMPORTANT_ADDRESS = 0x543d...;
    SomeContract someContract;
    constructor() {...}
}

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

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

Custom Error Reverts

  • Specify which custom error is expected on revert.

  • If the error returns a value, the test needs to encode the value using abi.encodeWithSelector to ensure the test passes.

contract DSC {
    error DSC__MintNotZeroAddress();
    error DSCEngine__HealthFactorIsBelowMinimum(uint256 healthFactor);
    
    function _revertIfHealthFactorIsBroken(address user) internal view {
        uint256 userHealthFactor = _healthFactor(user);
        if (userHealthFactor < MIN_HEALTH_FACTOR) {
            revert DSCEngine__HealthFactorIsBelowMinimum(userHealthFactor);
        }
    }
}

contract DSCTest is Test {
    function test_CantMintToZeroAddress() public {
        vm.prank(dsc.owner());
        vm.expectRevert(DSC.DSC__MintNotZeroAddress.selector);
        dsc.mint(address(0), 100);
    }

    function test_MintFailsIfHealthFactorIsBroken() public depositedCollateral {
        uint256 healthFactor = 2;
        
        bytes memory encodedRevert = abi.encodeWithSelector(
            DSCEngine.DSCEngine__HealthFactorIsBelowMinimum.selector,
            healthFactor
        );
        vm.startPrank(USER);
        vm.expectRevert(encodedRevert);
        dscEngine.mintDsc(usdAmount);
        vm.stopPrank();
    }
}

Event Testing

expectEmit

Unit Testing

forge test --summary
forge test -vvv
forge test --mc <SPECIFIC_TEST_CONTRACT_NAME>
forge test --mt <SPECIFIC_TEST_FUNCTION_NAME>
forge test --fork-url <RPC_URL>

Integration Testing

TODO

Forked Testing

To run all tests in a forked environment, such as a forked Ethereum mainnet, pass an RPC URL via the --fork-url flag.

forge test --fork-url <RPC_URL>

Forking is especially useful when you need to interact with existing contracts. You may choose to do integration testing this way, as if you were on an actual network.

Staging Testing

TODO

Fuzz Testing

function test_FuzzTestExample(
    uint256 randomRequestId
) public raffleEntered {
    vm.expectRevert("nonexistent request");
    VRFCoordinatorV2Mock(vrfCoordinatorV2).fulfillRandomWords(
        randomRequestId,
        address(raffle)
    );
}

TODO: Turn this video into notes.

https://www.youtube.com/watch?v=juyY-CTolac

Coverage

forge coverage
forge coverage --report debug
forge coverage --report debug > coverage-report.txt

Coverage line highlighting

forge coverage --report lcov
  • Open the command palette in VS Code (CMD+SHIFT+P or CTRL+SHIFT+P by default) and type โ€œDisplay Coverageโ€

TODO: Exponential value display

Last updated