# Foundry Forge vs Hardhat Testing — A Practical Side-by-Side
The Solidity testing landscape has two dominant players: Hardhat (the established JavaScript-based framework) and Foundry Forge (the new Rust-based speed demon). Both are excellent, but they approach testing in fundamentally different ways. This guide will help you choose the right tool for your project.
Overview
Hardhat
- Language: JavaScript/TypeScript
- Test Language: JavaScript/TypeScript
- Speed: Fast (JavaScript VM)
- Best For: Teams familiar with JS, complex scripting, frontend integration
- Ecosystem: Massive plugin ecosystem
Foundry Forge
- Language: Rust
- Test Language: Solidity
- Speed: Very fast (native execution)
- Best For: Pure Solidity devs, fuzzing, gas optimization, CI/CD
- Ecosystem: Growing fast, focused on testing
Installation & Setup
Hardhat
# Initialize project
mkdir my-project && cd my-project
npm init -y
# Install Hardhat
npm install --save-dev hardhat
# Initialize
npx hardhat init
# Project structure
my-project/
├── contracts/
├── test/
├── scripts/
└── hardhat.config.js
Foundry Forge
# Install Foundry
curl -L https://foundry.paradigm.xyz | bash
foundryup
# Initialize project
forge init my-project
cd my-project
# Project structure
my-project/
├── src/ # Contracts
├── test/ # Tests (also Solidity!)
├── script/ # Deployment scripts
└── foundry.toml
Winner: Foundry — Single command installation, no npm dependencies.
Test Syntax Comparison
Let's test the same contract with both frameworks:
// src/Counter.sol
pragma solidity ^0.8.0;
contract Counter {
uint256 public count;
function increment() public {
count++;
}
function decrement() public {
require(count > 0, "Count is zero");
count--;
}
function set(uint256 _count) public {
count = _count;
}
}
Hardhat Test
// test/Counter.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Counter", function () {
let counter;
beforeEach(async function () {
const Counter = await ethers.getContractFactory("Counter");
counter = await Counter.deploy();
await counter.deployed();
});
it("should start at zero", async function () {
expect(await counter.count()).to.equal(0);
});
it("should increment", async function () {
await counter.increment();
expect(await counter.count()).to.equal(1);
await counter.increment();
expect(await counter.count()).to.equal(2);
});
it("should decrement", async function () {
await counter.set(5);
await counter.decrement();
expect(await counter.count()).to.equal(4);
});
it("should revert on underflow", async function () {
await expect(counter.decrement())
.to.be.revertedWith("Count is zero");
});
});
Foundry Forge Test
// test/Counter.t.sol
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "../src/Counter.sol";
contract CounterTest is Test {
Counter counter;
function setUp() public {
counter = new Counter();
}
function testStartsAtZero() public {
assertEq(counter.count(), 0);
}
function testIncrement() public {
counter.increment();
assertEq(counter.count(), 1);
counter.increment();
assertEq(counter.count(), 2);
}
function testDecrement() public {
counter.set(5);
counter.decrement();
assertEq(counter.count(), 4);
}
function testRevertOnUnderflow() public {
vm.expectRevert("Count is zero");
counter.decrement();
}
}
Key Differences:
| Aspect | Hardhat | Forge |
|--------|---------|-------|
| Test Language | JavaScript | Solidity |
| Async/Await | Required | Not needed |
| Deployment | getContractFactory() | new Contract() |
| Assertions | chai: expect() | forge-std: assertEq() |
| Setup | beforeEach() | setUp() |
Speed Comparison
Let's benchmark on a real project (100 test cases):
Hardhat
npx hardhat test
# Output:
100 passing (45s)
Forge
forge test
# Output:
Running 100 tests for test/Counter.t.sol:CounterTest
[PASS] testIncrement() (gas: 12345)
[PASS] testDecrement() (gas: 15432)
...
Test result: ok. 100 passed; 0 failed; finished in 1.24s
Forge is ~36x faster on this benchmark. The gap widens with larger test suites.
Why Is Forge Faster?
Fuzzing
Hardhat (via Echidna integration)
Hardhat doesn't have built-in fuzzing. You need external tools:
npm install --save-dev @nomicfoundation/hardhat-echidna
# Or use Foundry's fuzzing with Hardhat
npm install --save-dev @nomicfoundation/hardhat-foundry
Forge (Built-in)
// test/Counter.t.sol
contract CounterFuzzTest is Test {
Counter counter;
function setUp() public {
counter = new Counter();
}
// Fuzz test: runs 256 times with random inputs
function testFuzz_SetCount(uint256 randomCount) public {
counter.set(randomCount);
assertEq(counter.count(), randomCount);
}
// Fuzz with constraints
function testFuzz_IncrementNeverOverflows(uint8 iterations) public {
for (uint8 i = 0; i < iterations; i++) {
counter.increment();
}
// Should never overflow
assertLe(counter.count(), type(uint256).max);
}
// Stateful fuzzing
function testFuzz_DecrementRequiresNonZero(uint256 initial, uint256 decrements) public {
vm.assume(decrements <= initial); // Constrain inputs
counter.set(initial);
for (uint256 i = 0; i < decrements; i++) {
counter.decrement();
}
assertEq(counter.count(), initial - decrements);
}
}
Run with custom iterations:
# Default: 256 runs
forge test
# Deeper fuzzing: 10,000 runs
forge test --fuzz-runs 10000
# Specific test
forge test --match-test testFuzz_SetCount -vvv
Winner: Forge — Fuzzing is first-class, fast, and trivial to use.
Debugging
Hardhat
// test/Counter.test.js
it("should debug", async function () {
console.log("Count before:", await counter.count());
await counter.increment();
console.log("Count after:", await counter.count());
});
Run with stack traces:
npx hardhat test --verbose
# Or use Hardhat console
npx hardhat console
> const Counter = await ethers.getContractFactory("Counter")
> const counter = await Counter.deploy()
> await counter.increment()
Forge
// test/Counter.t.sol
function testDebug() public {
console.log("Count before:", counter.count());
counter.increment();
console.log("Count after:", counter.count());
}
Run with verbosity levels:
# Basic logs
forge test -vv
# With traces
forge test -vvv
# With full opcodes
forge test -vvvv
# Debug specific test
forge test --debug testDebug
Forge's debugger is interactive:
forge test --debug testDebug
# Debugger opens:
# [OPCODE] PUSH1 0x01
# [STACK] [0, 1]
# [MEMORY] [0x00...0x20: 0]
#
# Commands:
# n - next opcode
# s - step into
# c - continue
# q - quit
Winner: Forge — Interactive debugger is a game-changer.
Fork Testing
Both frameworks excel at mainnet forking.
Hardhat
// hardhat.config.js
module.exports = {
networks: {
hardhat: {
forking: {
url: "https://eth-mainnet.alchemyapi.io/v2/YOUR_KEY",
blockNumber: 18000000
}
}
}
};
// test/ForkTest.js
const { expect } = require("chai");
describe("Uniswap Fork Test", function () {
it("should swap on Uniswap", async function () {
const [signer] = await ethers.getSigners();
// Mainnet Uniswap router address
const router = await ethers.getContractAt(
"IUniswapV2Router",
"0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"
);
// Perform swap...
});
});
Forge
# Fork test
forge test --fork-url https://eth-mainnet.alchemyapi.io/v2/YOUR_KEY
# Fork at specific block
forge test --fork-url $RPC_URL --fork-block-number 18000000
// test/ForkTest.t.sol
contract UniswapForkTest is Test {
IUniswapV2Router router;
function setUp() public {
// Mainnet router address
router = IUniswapV2Router(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D);
}
function testSwapOnFork() public {
address WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
// Prank as whale
address whale = 0x1234...;
vm.startPrank(whale);
// Perform swap
// ...
vm.stopPrank();
}
}
Forge's vm cheatcodes make fork testing powerful:
// Time travel
vm.warp(block.timestamp + 1 days);
// Change block number
vm.roll(block.number + 100);
// Impersonate account
vm.startPrank(vitalik);
// Set balance
vm.deal(address(this), 100 ether);
// Set storage slot
vm.store(address(token), slot, value);
Winner: Tie — Both are excellent, Forge has more cheatcodes.
Gas Reporting
Hardhat
npm install --save-dev hardhat-gas-reporter
// hardhat.config.js
require("hardhat-gas-reporter");
module.exports = {
gasReporter: {
enabled: true,
currency: "USD",
coinmarketcap: "YOUR_API_KEY"
}
};
Output:
·-----------------------|----------------------------|-------------|-----------------------------·
| Solc version: 0.8.20 · Optimizer enabled: true · Runs: 200 · Block limit: 30000000 gas │
························|····························|·············|······························
| Methods │
························|···············|·············|·············|··············|··············
| Contract · Method · Min · Max · Avg · # calls · usd (avg) │
························|···············|·············|·············|··············|··············
| Counter · incr... · 26789 · 43989 · 32456 · 10 · 1.23 │
Forge
Gas reporting is built-in:
forge test --gas-report
Output:
| src/Counter.sol:Counter contract | | | | | |
|----------------------------------|-----------------|-------|--------|-------|---------|
| Deployment Cost | Deployment Size | | | | |
| 123456 | 789 | | | | |
| Function Name | min | avg | median | max | # calls |
| decrement | 1234 | 2345 | 2345 | 3456 | 5 |
| increment | 1234 | 1456 | 1456 | 1678 | 10 |
| set | 5432 | 6543 | 6543 | 7654 | 3 |
Winner: Forge — No plugin needed, faster, cleaner output.
When to Use Each
Use Hardhat When:
Use Foundry Forge When:
Use Both When:
Many teams use both in different scenarios:
# Fast iteration with Forge
forge test --match-test testSwap -vv
# Integration tests with Hardhat
npx hardhat test test/integration/
# Deployment with Hardhat scripts
npx hardhat run scripts/deploy.js --network mainnet
# Security fuzzing with Forge
forge test --fuzz-runs 100000
Migration Path
Hardhat → Forge
# Install Foundry in Hardhat project
forge init --force
# Keep both configs
# - hardhat.config.js
# - foundry.toml
# Run Hardhat tests
npx hardhat test
# Run Forge tests
forge test
Forge → Hardhat
# Add Hardhat
npm init -y
npm install --save-dev hardhat
# Init Hardhat
npx hardhat init
# Keep Foundry
forge build && forge test
Conclusion
Both Foundry Forge and Hardhat are exceptional tools. Your choice depends on your team's expertise and project needs:
Choose Forge if: Speed, fuzzing, and Solidity-native development are priorities.
Choose Hardhat if: JavaScript ecosystem integration and mature tooling are critical.
Choose both if: You want the best of both worlds (many top teams do this).
Start experimenting today — you can have both installed and running in under 5 minutes.