Hashtag Web3 Logo

Testing Smart Contracts

9 min
intermediate

Why Smart Contract Testing Is Different

In traditional software, bugs are annoying. In smart contracts, bugs are catastrophic. A deployed contract is immutable โ€” once it's on Ethereum, the code cannot be changed. If someone finds a vulnerability, they can drain the contract's entire balance before anyone can react.

There is no rollback. There is no hotfix. There is no "we'll patch it in the next release."

This is why smart contract testing is not optional. It is the primary line of defense.

๐Ÿงช Unit Tests Single functions Hardhat / Foundry ๐ŸŽฒ Fuzz Tests Random inputs Foundry native ๐Ÿ”— Integration Multi-contract Fork mainnet ๐Ÿ›ก๏ธ Audit External review Before mainnet

Testing with Hardhat (JavaScript)

Hardhat is the most popular development framework for Ethereum. Tests are written in JavaScript or TypeScript using Mocha and Chai.

A Simple Token Test

const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("SimpleToken", function () {
 let token, owner, alice;

 beforeEach(async function () {
 [owner, alice] = await ethers.getSigners();
 const Token = await ethers.getContractFactory("SimpleToken");
 token = await Token.deploy(1000); // mint 1000 tokens to deployer
 });

 it("should assign total supply to the owner", async function () {
 const balance = await token.balanceOf(owner.address);
 expect(balance).to.equal(1000);
 });

 it("should transfer tokens between accounts", async function () {
 await token.transfer(alice.address, 100);
 expect(await token.balanceOf(alice.address)).to.equal(100);
 expect(await token.balanceOf(owner.address)).to.equal(900);
 });

 it("should revert when sender has insufficient balance", async function () {
 await expect(
 token.connect(alice).transfer(owner.address, 1)
 ).to.be.revertedWith("Insufficient balance");
 });
});

What this tests:

  1. Initial state is correct (deployer gets all tokens).
  2. The core function works (transfer moves tokens).
  3. Error handling works (transferring more than you have reverts).

Run with: npx hardhat test

Testing with Foundry (Solidity)

Foundry lets you write tests in Solidity itself. This is faster (no JavaScript overhead) and gives you access to powerful features like fuzz testing and cheatcodes.

The Same Test in Foundry

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

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

contract SimpleTokenTest is Test {
 SimpleToken token;
 address alice = makeAddr("alice");

 function setUp() public {
 token = new SimpleToken(1000);
 }

 function test_OwnerGetsInitialSupply() public view {
 assertEq(token.balanceOf(address(this)), 1000);
 }

 function test_Transfer() public {
 token.transfer(alice, 100);
 assertEq(token.balanceOf(alice), 100);
 assertEq(token.balanceOf(address(this)), 900);
 }

 function test_RevertOnInsufficientBalance() public {
 vm.prank(alice); // next call comes from alice
 vm.expectRevert("Insufficient balance");
 token.transfer(address(this), 1);
 }
}

Run with: forge test

Fuzz Testing

Fuzz testing is where Foundry excels. Instead of testing with hardcoded values, you let the framework generate random inputs.

function testFuzz_TransferNeverExceedsSupply(uint256 amount) public {
 // Bound the amount to something reasonable
 amount = bound(amount, 0, token.totalSupply());

 token.transfer(alice, amount);

 // Invariant: total supply should never change
 assertEq(
 token.balanceOf(address(this)) + token.balanceOf(alice),
 token.totalSupply()
 );
}

Foundry will run this function with hundreds of random amount values โ€” including 0, 1, max uint256, and everything in between. If any input breaks the invariant, Foundry reports the exact failing case.

What to Test: A Checklist

CategoryWhat to verifyExample
Access controlOnly authorized addresses can call admin functionsonlyOwner modifier works; random users get reverted
State changesFunctions modify storage correctlyAfter deposit, balance increases by exact amount
Edge casesZero amounts, max values, empty arraysTransferring 0 tokens doesn't break anything
RevertsInvalid operations fail cleanlyWithdrawing more than your balance reverts
ReentrancyExternal calls don't allow re-entryCallback during withdrawal can't drain the contract
EventsCorrect events are emittedTransfer event fires with correct from, to, amount

Key takeaways

  • Smart contract bugs are permanent and expensive. Testing is the first and most important layer of security.
  • Hardhat uses JavaScript tests (familiar, good ecosystem). Foundry uses Solidity tests (faster, native fuzz testing).
  • Fuzz testing automatically generates random inputs to catch edge cases you would never think of manually.
  • Always test access control first โ€” who can call what, and what happens when unauthorized users try.

Quiz: Testing Smart Contracts

1 / 5

Why is testing critical for smart contracts specifically?