Comparaison·12 min de lecture·Par Solingo

Foundry Forge vs Hardhat Testing — A Practical Side-by-Side

An in-depth comparison of Foundry Forge and Hardhat testing frameworks, including syntax, speed, fuzzing, debugging, and when to use each.

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

  • No JavaScript overhead: Direct Rust → EVM execution
  • No RPC calls: Tests run in-process
  • Parallel execution: Tests run concurrently by default
  • Optimized EVM: Foundry's EVM implementation is highly tuned
  • 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:

  • Team knows JavaScript: Learning curve is lower
  • Complex scripting needed: Deployment, automation, data processing
  • Frontend integration: React/Vue tests alongside contract tests
  • Extensive plugin ecosystem: Need specific integrations
  • TypeScript support: Strong typing for tests
  • Use Foundry Forge When:

  • Pure Solidity development: Team prefers Solidity for everything
  • Speed is critical: Large test suites, CI/CD pipelines
  • Fuzzing is important: DeFi protocols, security-critical code
  • Gas optimization: Need detailed gas reports
  • Learning Solidity: Writing tests in Solidity improves Solidity skills
  • 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.

    Prêt à mettre en pratique ?

    Applique ces concepts avec des exercices interactifs sur Solingo.

    Commencer gratuitement