# Tests Foundry vs Hardhat — Comparaison Pratique Côte à Côte
En 2026, deux frameworks dominent le testing de smart contracts : Hardhat (l'établi, basé sur JavaScript) et Foundry (le challenger, basé sur Solidity). Comparaison objective sans fanboy wars.
Setup Initial
Hardhat
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox
npx hardhat init # Choose "TypeScript project"
Structure générée :
project/
├── contracts/
├── test/
├── scripts/
├── hardhat.config.ts
└── package.json
Foundry
forge init my-project
cd my-project
Structure générée :
project/
├── src/
├── test/
├── script/
├── lib/
└── foundry.toml
Temps d'installation :
- Hardhat : ~45 secondes (npm install)
- Foundry : ~2 secondes (binary Rust)
Test Basique : Token Transfer
Contrat à tester :
// SimpleToken.sol
contract SimpleToken {
mapping(address => uint256) public balances;
function mint(address to, uint256 amount) public {
balances[to] += amount;
}
function transfer(address to, uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
balances[to] += amount;
}
}
Hardhat (TypeScript)
// test/SimpleToken.test.ts
import { expect } from "chai";
import { ethers } from "hardhat";
import { SimpleToken } from "../typechain-types";
import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers";
describe("SimpleToken", function () {
let token: SimpleToken;
let owner: SignerWithAddress;
let alice: SignerWithAddress;
let bob: SignerWithAddress;
beforeEach(async function () {
[owner, alice, bob] = await ethers.getSigners();
const Token = await ethers.getContractFactory("SimpleToken");
token = await Token.deploy();
});
it("should mint tokens", async function () {
await token.mint(alice.address, 1000);
expect(await token.balances(alice.address)).to.equal(1000);
});
it("should transfer tokens", async function () {
await token.mint(alice.address, 1000);
await token.connect(alice).transfer(bob.address, 300);
expect(await token.balances(alice.address)).to.equal(700);
expect(await token.balances(bob.address)).to.equal(300);
});
it("should revert on insufficient balance", async function () {
await expect(
token.connect(alice).transfer(bob.address, 100)
).to.be.revertedWith("Insufficient balance");
});
});
Run :
npx hardhat test
# Time: ~3.5 seconds
Foundry (Solidity)
// test/SimpleToken.t.sol
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "../src/SimpleToken.sol";
contract SimpleTokenTest is Test {
SimpleToken token;
address alice = address(0x1);
address bob = address(0x2);
function setUp() public {
token = new SimpleToken();
}
function test_Mint() public {
token.mint(alice, 1000);
assertEq(token.balances(alice), 1000);
}
function test_Transfer() public {
token.mint(alice, 1000);
vm.prank(alice);
token.transfer(bob, 300);
assertEq(token.balances(alice), 700);
assertEq(token.balances(bob), 300);
}
function testFail_InsufficientBalance() public {
vm.prank(alice);
token.transfer(bob, 100);
}
// Alternative avec vm.expectRevert
function test_RevertInsufficientBalance() public {
vm.prank(alice);
vm.expectRevert("Insufficient balance");
token.transfer(bob, 100);
}
}
Run :
forge test
# Time: ~0.2 seconds (17x plus rapide!)
Vitesse de Test : Benchmark
Sur un projet de 50 tests (Uniswap V2 fork) :
| Framework | Temps Total | Par Test | Parallélisation |
|-----------|-------------|----------|-----------------|
| Hardhat | 45s | ~0.9s | Non (séquentiel) |
| Foundry | 2.1s | ~0.04s | Oui (auto) |
Winner : Foundry (21x plus rapide)
Raison : Foundry compile en bytecode natif et parallélise, Hardhat utilise une VM JavaScript.
Fuzzing : La Killer Feature de Foundry
Hardhat (pas de fuzzing natif)
Il faut utiliser une librairie tierce :
import fc from "fast-check";
it("should never overflow", async function () {
await fc.assert(
fc.asyncProperty(
fc.integer({ min: 0, max: 1e9 }),
fc.integer({ min: 0, max: 1e9 }),
async (a, b) => {
await token.mint(alice.address, a);
if (a >= b) {
await expect(token.connect(alice).transfer(bob.address, b))
.to.not.be.reverted;
}
}
)
);
});
Foundry (fuzzing natif)
function testFuzz_Transfer(uint256 amount, uint256 transferAmount) public {
vm.assume(transferAmount <= amount); // Filter inputs
token.mint(alice, amount);
vm.prank(alice);
token.transfer(bob, transferAmount);
assertEq(token.balances(alice), amount - transferAmount);
assertEq(token.balances(bob), transferAmount);
}
Run :
forge test --match-test testFuzz
# Runs 256 random scenarios by default
Configuration :
# foundry.toml
[fuzz]
runs = 10000 # Number of scenarios
max_test_rejects = 100000
Winner : Foundry (fuzzing first-class, Hardhat nécessite workarounds)
Debugging
Hardhat
// Hardhat console.log (dans le contrat!)
import "hardhat/console.sol";
contract SimpleToken {
function transfer(address to, uint256 amount) public {
console.log("Transfer from", msg.sender, "to", to);
console.log("Amount:", amount);
// ...
}
}
npx hardhat test
# Logs appear in stdout
Foundry
import "forge-std/console.sol";
contract SimpleToken {
function transfer(address to, uint256 amount) public {
console.log("Transfer from", msg.sender, "to", to);
console.log("Amount:", amount);
// ...
}
}
forge test -vvvv
# -v : show test results
# -vv : show logs for failing tests
# -vvv : show logs for all tests
# -vvvv: show traces (call stack)
Trace example :
[PASS] test_Transfer() (gas: 52341)
Traces:
[52341] SimpleTokenTest::test_Transfer()
├─ [24532] SimpleToken::mint(0x1, 1000)
│ └─ ← ()
├─ [0] VM::prank(0x1)
│ └─ ← ()
├─ [21653] SimpleToken::transfer(0x2, 300)
│ └─ ← ()
└─ ← ()
Winner : Foundry (traces détaillées incluses, Hardhat nécessite plugins)
Fork Testing
Tester contre l'état actuel de mainnet.
Hardhat
// hardhat.config.ts
export default {
networks: {
hardhat: {
forking: {
url: "https://eth-mainnet.alchemyapi.io/v2/YOUR_KEY",
blockNumber: 19000000
}
}
}
};
// test/Fork.test.ts
it("should swap on Uniswap V3", async function () {
const router = await ethers.getContractAt(
"ISwapRouter",
"0xE592427A0AEce92De3Edee1F18E0157C05861564"
);
// Impersonate whale
await network.provider.request({
method: "hardhat_impersonateAccount",
params: ["0x123...whale"]
});
const whale = await ethers.getSigner("0x123...whale");
await router.connect(whale).exactInputSingle({...});
});
Foundry
contract ForkTest is Test {
ISwapRouter router = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564);
address whale = 0x123...;
function setUp() public {
vm.createSelectFork("https://eth-mainnet.alchemyapi.io/v2/YOUR_KEY");
// Or use alias: vm.createSelectFork("mainnet");
}
function test_SwapOnUniswap() public {
vm.prank(whale);
router.exactInputSingle({...});
// Assertions...
}
}
Cache : Foundry cache automatiquement les appels RPC dans ~/.foundry/cache.
Winner : Égalité (les deux excellents, Foundry un peu plus simple)
Cheatcodes : Le Superpouvoir de Foundry
Hardhat a des helpers, Foundry a des cheatcodes (via vm.*) :
// Time manipulation
vm.warp(block.timestamp + 7 days);
// Set block number
vm.roll(19000000);
// Set balance
vm.deal(alice, 100 ether);
// Mock calls
vm.mockCall(
address(oracle),
abi.encodeWithSelector(IOracle.getPrice.selector),
abi.encode(1500e18)
);
// Expect events
vm.expectEmit(true, true, false, true);
emit Transfer(alice, bob, 100);
token.transfer(bob, 100);
// Snapshot state
uint256 snapshot = vm.snapshot();
token.transfer(bob, 100);
vm.revertTo(snapshot); // Rollback state
Hardhat nécessite des plugins pour la plupart de ces features.
Coverage
Hardhat
npm install --save-dev solidity-coverage
npx hardhat coverage
Output HTML détaillé (branches, statements, functions).
Foundry
forge coverage
# Or detailed report
forge coverage --report lcov
genhtml lcov.info -o coverage/
Winner : Hardhat (coverage HTML plus détaillé, Foundry en rattrapage)
Gas Reporting
Hardhat
npm install hardhat-gas-reporter
// hardhat.config.ts
import "hardhat-gas-reporter";
export default {
gasReporter: {
enabled: true,
currency: "USD",
coinmarketcap: "YOUR_API_KEY"
}
};
Foundry
forge test --gas-report
Output :
| Function | avg | median | max |
|---------------|--------|--------|--------|
| mint | 24532 | 24532 | 24532 |
| transfer | 21653 | 21653 | 29653 |
Winner : Égalité (Hardhat plus joli, Foundry plus rapide)
Invariant Testing (Property-Based)
Foundry only (Hardhat n'a pas d'équivalent natif) :
contract InvariantTest is Test {
SimpleToken token;
Handler handler;
function setUp() public {
token = new SimpleToken();
handler = new Handler(token);
targetContract(address(handler));
}
// Invariant: sum of balances == total minted
function invariant_SumOfBalances() public {
assertEq(handler.sumOfBalances(), handler.totalMinted());
}
}
contract Handler {
SimpleToken token;
uint256 public totalMinted;
mapping(address => uint256) public balances;
function mint(address to, uint256 amount) public {
token.mint(to, amount);
balances[to] += amount;
totalMinted += amount;
}
function sumOfBalances() public view returns (uint256) {
// Iterate all addresses...
}
}
forge test --match-test invariant
Winner : Foundry (feature unique)
Verdict Final
| Critère | Hardhat | Foundry | Winner |
|---------|---------|---------|--------|
| Setup | Simple | Plus simple | Foundry |
| Vitesse | Lent | Ultra-rapide | Foundry |
| Fuzzing | Plugin | Natif | Foundry |
| Debugging | Console.log | Traces | Foundry |
| Fork Testing | Excellent | Excellent | Égalité |
| Coverage | Détaillé | Basique | Hardhat |
| Gas Report | Joli | Fonctionnel | Hardhat |
| Invariants | ❌ | ✅ | Foundry |
| Écosystème | Mature | En croissance | Hardhat |
| Courbe d'apprentissage | Facile (JS) | Moyenne (Solidity) | Hardhat |
Recommandation 2026
Utilisez Foundry si :
- ✅ Vous voulez de la vitesse (dev rapide, CI/CD)
- ✅ Vous faites du fuzzing/invariant testing
- ✅ Vous préférez coder en Solidity
- ✅ Vous êtes sur un nouveau projet
Utilisez Hardhat si :
- ✅ Vous avez une codebase TypeScript existante
- ✅ Votre équipe préfère JavaScript
- ✅ Vous avez besoin de plugins spécifiques (Tenderly, Defender, etc.)
- ✅ Vous voulez un coverage HTML détaillé
La vérité : beaucoup de projets utilisent les deux (tests Foundry, deploy Hardhat).
Testez par vous-même et choisissez ce qui matche votre workflow.