⚒️ Testing Best Practices in Foundry

Paul Berg @ EthCC 2023

▶️ Watch on YouTube

Personal Background

  • ⏳ Co-founder and Solidity Lead at Sablier Labs
  • 👨‍💻 Ethereum developer for 5+ years
  • 🐦 Sharing Solidity tips on Twitter @PaulRBerg
  • 🚀 Sablier V2: 100% built with Foundry

🐣 Foundry

Foundry made its debut in 2022. Before this, Solidity was rarely used for testing smart contracts.

Today, many Foundry test suites are disorganized

😇 How it starts

              
                function test_Foo() external {
                  uint256 x = 42;
                  assertEq(contract.foo(x), x, "value mismatch");
                }
              
            

😵‍💫 How it ends (monolith)

              
                function test_Foo_1() external {
                  // --- snip ---
                }

                function test_Foo_2() external {
                  // --- snip ---
                }

                function test_Foo_3(uint256 arg0) external {
                  // --- snip ---
                }

                function test_Foo_4(uint256 arg0, uint256 arg1) external {
                  // --- snip ---
                }
              
            

Foundry test categories

Create a directory for each of the following categories:

Category Description
Unit Functions involving a single contract
Integration Function involving multiple contracts
Invariant Expressions that should always hold true
Fork Tests running against a production chain

Foundry test subcategories

Sub-category Description
Concrete Standard deterministic tests that take no inputs
Fuzz Non-deterministic tests with fuzzed inputs

Low-level design and spec

  • Branching Tree Technique
  • Use a test function naming convention like this:
    • test_Foo
    • testFuzz_Foo
    • test_RevertWhen_Foo
    • testFuzz_RevertWhen_Foo

🌳 Branching Tree Technique (BTT)

  1. Target a function
  2. Create a ".tree" file
  3. Consider all possible execution paths
  4. Consider what contract state leads to what paths
  5. Consider what function params lead to what paths
  6. Define "given state is x" nodes
  7. Define "when parameter is x" nodes
  8. Define final "it should" tests

Example: statusOf.tree

              
              ├── when the id references a null stream
              │  └── it should revert
              └── when the id does not reference a null stream
                 ├── given assets have been fully withdrawn
                 │  └── it should return DEPLETED
                 └── given assets have not been fully withdrawn
                    ├── given the stream has been canceled
                    │  └── it should return CANCELED
                    └── given the stream has not been canceled
                       ├── given the start time is in the future
                       │  └── it should return PENDING
                       └── given the start time is not in the future
                          ├── given the refundable amount is zero
                          │  └── it should return SETTLED
                          └── given the refundable amount is not zero
                             └── it should return STREAMING
					    
            

Example: statusOf

              
                function test_RevertWhen_Null() external {
                    uint256 nullStreamId = 1729;
                    vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2Lockup_Null.selector, nullStreamId));
                    lockup.statusOf(nullStreamId);
                }

                modifier whenNotNull() {
                    defaultStreamId = createDefaultStream();
                    _;
                }

                function test_StatusOf()
                    external
                    whenNotNull
                    givenAssetsNotFullyWithdrawn
                    givenStreamNotCanceled
                    givenStartTimeNotInTheFuture
                    givenRefundableAmountNotZero
                {
                    vm.warp({ timestamp: defaults.START_TIME() + 1 seconds });
                    Lockup.Status actualStatus = lockup.statusOf(defaultStreamId);
                    Lockup.Status expectedStatus = Lockup.Status.STREAMING;
                    assertEq(actualStatus, expectedStatus);
                }
					    
            

Specification Techniques

Technique Barrier to entry Effectiveness
Braching Tree Technique Entry-level Moderately effective
Cucumber Gherkin Medium-level Moderately effective
Certora, TLA+ Senior-level Highly effective

The End

V2 Core