How to test smart contracts using Foundry

How to test smart contracts using Foundry

Learn how you can test your smart contracts in Foundry

Introduction

If you’ve been following this series, you know how can write, compile, deploy and verify smart contracts using Foundry. However, it is crucial for all smart contract developers to know how to test their contracts before deploying them on live networks that use real funds like Ethereum Mainnet. In this guide, we’ll be learning how to do just that using Foundry’s test suite. Keep in mind that this will cover a very simple example and in a real-world dApp, the tests will be much more rigid and extensive since loss of funds can simply NOT be tolerated. This guide will be a very short one.

Initial Setup

Open your terminal, initialize a new Foundry project and switch to the created directory, like so:

forge init test-proj
cd test-proj

Take a look at the structure of the directory. You will notice that there is a folder called “test” already present there. On opening it, you’ll see a file called Counter.t.sol.

Just like scripts are suffixed with .s, tests are suffixed with .t. This is just a naming convention used to denote the contract the test file is testing. So, for example, if you had a file called Voting.sol, you would name its corresponding test file Voting.t.sol going by convention and you would put it inside the test folder.

Modifying the contract

Since there are test cases already written for us, let’s make some changes in the contract which will consequently nudge us towards changing the test cases too. Paste this code in src/Counter.sol:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

contract Counter {
    int256 public number;

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

    function increment() public {
        number++;
    }

    function decrement() public{ 
        number--;
    }
}

We changed the data type of number to int instead of uint and added a decrement function to the contract. Now, let’s move on to the test cases

Contract tests

Open test/Counter.t.sol. You will see this code in the file:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

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

contract CounterTest is Test {
    Counter public counter;

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

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

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

setUp is an optional function that is invoked before each test case. In the above code, setUp creates a new instance of the Counter contract and also calls its setNumber function with 0 as the function parameter. This sets the value of the number variable to 0.

test_ is a prefix that is used to specify test functions

test_Increment calls the increment function of the contract and then assertEq is a keyword provided by the Forge Standard Library that lets us compare the value expected and the value actually obtained. In test_Increment, we specify that the value of number should be 1 after calling the increment function.

testFuzz_ is a prefix that is used for specifying functions that use fuzz testing. In fuzz testing functions, you specify the type and the name of the variable that is needed in the test function and Foundry assigns a random value of the specified type to the variable. Fuzz testing is quite helpful for figuring out edge cases that may have escaped you and is not exclusive to smart contract development.

testFuzz_SetNumber gets a random uint value and tries to call the contract’s setNumber function with that as the parameter. The assertion specifies that the value of number after this should be equal to the random value.

Before we proceed, make this small change in the test:

    function testFuzz_SetNumber(int256 x) public {

We just changed uint256 to int256 since we made similar changes in our contract.

Great, now let’s add a new test for testing the decrement function. Add this to the test file:

    function test_Decrement() public{
        counter.decrement();
        assertEq(counter.number(), -1);
        console.log(counter.number());
    }

This test calls the decrement function and the assertion specifies that the value of number should be -1 after it. Here, we are also using console.log, which is a function provided to us via the Forge Standard Library and it is similar to Hardhat’s console functions. This will log out the value of number.

Running the Tests

Cool, we have understood how tests work and written one too. Now let’s see how we can actually run these tests. The command is:

forge test

Verbosity of logs

There are different levels of detail, ie. verbosity, in logs available. There are 5 levels of verbosity available and they are specified using flags:

  1. -v: Print test names and results

  2. -vv: Print logs for all tests.

  3. -vvv: Print execution traces for failing tests.

  4. -vvvv: Print execution traces for all tests, and setup traces for failing tests.

  5. -vvvvv: Print execution and setup traces for all tests, including storage changes.

Let’s see the difference between Level 1 and Level 5 for our tests

Level 1

forge test -v

Level 5

forge test -vvvvv

Yep. Level 5 is much more detailed

Conclusion

Congratulations! You’ve successfully learned how you can write and run tests for your smart contracts. Feel free to add more functions/tests to your code and also experiment with different levels of verbosity. There is just one more article left in Foundry Mode and I hope you’ll be there to witness the finale. Till then, stay tuned. Thanks a lot for reading to the end 🫡🫡