1

Context

I'm logging how often each scenario/case/branch of a fuzz test is hit/reached to an output file. The fuzz tests currently can lead to false positives (where the test passes because the scenario/case/branch on which it would fail, is not reached). To resolve this I currently manually check the logs of the fuzz test to see whether the relevant scenarios/cases/branches are hit/reached by the fuzz tests.

Objective

I would like to include an additional test at the end of the fuzz test(s) that throws an error if the required/relevant scenario/case/branch is not hit/reached in the fuzz test.

Issue

I do not know of any afterEach nor afterAll method in Foundry. I assume the tests should be runnable in random order, meaning there cannot be 1 test that is always executed last.

Constraints

I would prefer not running an additional script that latches onto the forge command, and I would prefer to include the additional desired check, within Solidity Foundry.

Question

How can I run a function after all the fuzz test code is completed?

(Kind of like the oposite of the setUp() function (except that is ran every time before each fuzz run instead once before all fuzz runs, (even though one can determine which of those fuzz runs is the first within the setUp() function)).

1 Answer 1

1

You can potentially use the afterInvariant function of the Invariant Testing (i.e., Stateful Fuzz Testing).

Here's the minimal example showcasing the invariant testing of the Counter contract (i.e., the contract that is created by default after executing forge init command):

src/Counter.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract Counter {
    uint256 public number;

    function setNumber(uint256 newNumber) public {
        number = newNumber;
    }

    function increment() public {
        number++;
    }
}

test/Handler.t.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {Test, console} from "forge-std/Test.sol";
import {Counter} from "../src/Counter.sol";

contract Handler is Test {
    Counter public counter;

    bool public isIncrementTested;
    bool public isSetNumberTested;
    uint256 public numberOfTestRunsForIncrement;
    uint256 public numberOfTestRunsForSetNumber;

    constructor() {
        counter = new Counter();
    }

    function increment() public {
        uint256 numberBeforeIncrement = counter.number();
        if (numberBeforeIncrement < type(uint256).max) {
            counter.increment();
            assertEq(counter.number(), numberBeforeIncrement + 1);
        }
        isIncrementTested = true;
        numberOfTestRunsForIncrement++;
    }

    function setNumber(uint256 x) public {
        counter.setNumber(x);
        assertEq(counter.number(), x);
        isSetNumberTested = true;
        numberOfTestRunsForSetNumber++;
    }
}

test/Invariants.t.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {Test, console} from "forge-std/Test.sol";
import {StdInvariant} from "forge-std/StdInvariant.sol";
import {Handler} from "./Handler.t.sol";
// import {Counter} from "../src/Counter.sol";

contract InvariantsTest is StdInvariant, Test {
    Handler public handler;

    function setUp() public {
        handler = new Handler();
        targetContract(address(handler));
    }

    function invariant_test() public pure {}

    function afterInvariant() public view {
        console.log(handler.isIncrementTested(), handler.isSetNumberTested());
        console.log(handler.numberOfTestRunsForIncrement(), handler.numberOfTestRunsForSetNumber());

        assertEq(handler.isIncrementTested(), true);
        assertEq(handler.isSetNumberTested(), true);
        assertEq(handler.numberOfTestRunsForIncrement() + handler.numberOfTestRunsForSetNumber(), 128);
    }
}

Add the configuration for invariant testing in your foundry.toml file:

[invariant]
runs = 128
depth = 128
fail_on_revert = true

Explanation

In the Handler contract:

  • increment() and setNumber() functions are defined, that are basically testing the corresponding functions of the Counter contract by passing in random inputs as arguments.

  • And, isIncrementTested, isSetNumberTested, numberOfTestRunsForIncrement and numberOfTestRunsForSetNumber are the handler ghost variables that are basically tracking the test calls.

In the InvariantsTest contract:

  • invariant_test() function is supposed to execute and test all the functions defined in the Handler contract (i.e., the target contract of the invariant testing) with random inputs as arguments.

  • afterInvariant() function would be executed after each invariant test (as in this case there is only one inviant test i.e., invariant_test()). As per the docs:

    afterInvariant() function is called at the end of each invariant run (if declared), allowing post campaign processing. This function can be used for logging campaign metrics (e.g. how many times a selector was called) and post fuzz campaign testing (e.g. close out all positions and assert all funds are able to exit the system).

1
  • Thank you for your detailed answer! I ran into some difficulties applying it. For completeness, I made a separate question to resolve those.
    – a.t.
    Commented Oct 22 at 20:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.