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
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.
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
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:
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...