Comparaison·11 min de lecture·Par Solingo

Tests Foundry vs Hardhat — Comparaison Pratique Côte à Côte

Comparaison détaillée des frameworks de test Foundry et Hardhat : syntaxe, vitesse, fuzzing, debugging et fork testing.

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

Prêt à mettre en pratique ?

Applique ces concepts avec des exercices interactifs sur Solingo.

Commencer gratuitement