Skip to content

Testing Smart Contracts with Foundry

Testing in Foundry works different than in Truffle or Hardhat. The tests in Foundry are written in Solidity. Now you're probably wondering, how can you test several sender accounts in Foundry?

That works with VM Cheatcodes.

Let's add a simple test and see how they perform.

Adding a test in Foundry

Add the following file to test/Spacebear.test.sol

pragma solidity 0.8.10;

import "forge-std/Test.sol";
import "../src/Spacebears.sol";

contract SpacebearsTest is Test {

    Spacebear spacebear;
    function setUp() public {
        spacebear = new Spacebear();
    }

    function testNameIsSpacebear() public {
        assertEq(spacebear.name(), "Spacebear");
    }
}

then run

forge test

forge test
forge test

Minting an NFT in Foundry

Let's mint our NFT and see if we're the rightful owner. Add the following parts to the test:

pragma solidity ^0.8.4;

import "forge-std/Test.sol";
import "../src/Spacebears.sol";

contract SpacebearsTest is Test {

    Spacebear spacebear;
    function setUp() public {
        spacebear = new Spacebear();
    }

    function testNameIsSpacebear() public {
        assertEq(spacebear.name(), "Spacebear");
    }

    function testMintingNFTs() public {
        spacebear.safeMint(msg.sender);
        assertEq(spacebear.ownerOf(0), msg.sender);
        assertEq(spacebear.tokenURI(0),  "https://ethereum-blockchain-developer.com/2022-06-nft-truffle-hardhat-foundry/nftdata/spacebear_1.json");
    }
}

If you run that test, it will surprisingly fail.

failed test run
failed test run

But you don't know where. If you run the command with -vv in addition, you can get insights (going up to -vvvv):

forge test -vv

forge test -vv
forge test -vv

Interestingly it fails at generating the URI, it completely ignores the uint. It works, interestingly, in truffle and hardhat though. Let's add an explicit String conversion. Luckily that's fairly easy with openzeppelin. Add the following to src/Spacebears.sol:

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

import "openzeppelin-contracts/token/ERC721/ERC721.sol";
import "openzeppelin-contracts/access/Ownable.sol";
import "openzeppelin-contracts/utils/Counters.sol";

import "openzeppelin-contracts/utils/Strings.sol";

contract Spacebear is ERC721, Ownable {
    using Counters for Counters.Counter;

    Counters.Counter private _tokenIdCounter;

    constructor() ERC721("Spacebear", "SBR") {}

    function _baseURI() internal pure override returns (string memory) {
        return "https://ethereum-blockchain-developer.com/2022-06-nft-truffle-hardhat-foundry/nftdata/";
    }

    function safeMint(address to) public onlyOwner {
        uint256 tokenId = _tokenIdCounter.current();
        _tokenIdCounter.increment();
        _safeMint(to, tokenId);
    }

    function buyToken() public payable {
        uint256 tokenId = _tokenIdCounter.current();
        require(msg.value == tokenId * 0.1 ether, "Not enough funds sent");

        _tokenIdCounter.increment();
        _safeMint(msg.sender, tokenId);
    }

    // The following functions are overrides required by Solidity.

    function _burn(uint256 tokenId) internal override(ERC721) {
        super._burn(tokenId);
    }

    function tokenURI(uint256 tokenId)
        public
        pure
        override(ERC721)
        returns (string memory)
    {
        return string(abi.encodePacked(_baseURI(),"spacebear_",Strings.toString(tokenId+1),".json"));
    }
}

run the test again, and it will pass:

successful unit test using Foundry
successful unit test using Foundry

Testing NFT Transfers with VM Pranking in Foundry

So, how do you test the transfer from address 1 to address 2?

With VM Pranking.

VM Pranking is a special contract command that instructs the VM to use a different account as msg.sender for example. vm.prank(address) would send the next transaction with the address given. If you do vm.startPrank(address) you can send several instructions with a new address

Let me show you...

pragma solidity ^0.8.4;

import "forge-std/Test.sol";
import "../src/Spacebears.sol";

contract SpacebearsTest is Test {

    Spacebear spacebear;
    function setUp() public {
        spacebear = new Spacebear();
    }

    function testNameIsSpacebear() public {
        assertEq(spacebear.name(), "Spacebear");
    }

    function testMintingNFTs() public {
        spacebear.safeMint(msg.sender);
        assertEq(spacebear.ownerOf(0), msg.sender);
        assertEq(spacebear.tokenURI(0),  "https://ethereum-blockchain-developer.com/2022-06-nft-truffle-hardhat-foundry/nftdata/spacebear_1.json");
    }

    function testNftCreationWrongOwner() public {
        vm.startPrank(address(0x1));
        vm.expectRevert("Ownable: caller is not the owner");
        spacebear.safeMint(address(0x1));
        vm.stopPrank();
    }
}

That works, but how can you buy an NFT. pranking is just setting the address to another account - how do you get balance? In testing you can use the same cheatcode prank, while in anvil you'd use another VM cheatcode, called hoax(address), there you get some ether too. Here we can keep using prank

pragma solidity ^0.8.4;

import "forge-std/Test.sol";
import "forge-std/Vm.sol";
import "../src/Spacebears.sol";

contract SpacebearsTest is Test {

    Spacebear spacebear;
    function setUp() public {
        spacebear = new Spacebear();
    }

    function testNameIsSpacebear() public {
        assertEq(spacebear.name(), "Spacebear");
    }

    function testMintingNFTs() public {
        spacebear.safeMint(msg.sender);
        assertEq(spacebear.ownerOf(0), msg.sender);
        assertEq(spacebear.tokenURI(0),  "https://ethereum-blockchain-developer.com/2022-06-nft-truffle-hardhat-foundry/nftdata/spacebear_1.json");
    }

    function testNftCreationWrongOwner() public {
        vm.startPrank(address(0x1));
        vm.expectRevert("Ownable: caller is not the owner");
        spacebear.safeMint(address(0x1));
        vm.stopPrank();
    }
    function testNftBuyToken() public {
        vm.startPrank(address(0x1));
        spacebear.buyToken();
        vm.stopPrank();
        assertEq(spacebear.ownerOf(0), address(0x1));
    }
}

If you run the test, it should just pass. But how can you deploy the token to GΓΆrli? That's what we gonna do in the next lecture...


Last update: July 27, 2023