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:
-v: Print test names and results
-vv: Print logs for all tests.
-vvv: Print execution traces for failing tests.
-vvvv: Print execution traces for all tests, and setup traces for failing tests.
-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 🫡🫡