Choose Foundry or Hardhat for Solidity Unit Testing
We've all been there. You're deep into a critical integration, trying to simulate a devious reentrancy attack in your test suite, and the whole process grinds to a halt.

You've Hit a Wall: Your Tests Are Flaky, Slow, and You're Tired of Wrestling with JavaScript for Solidity Logic
The choice of your testing framework isn't a minor preference—it's a foundational architectural decision that shapes your development velocity, the types of bugs you can catch, and your team's daily sanity. Let's break down the two titans of the Solidity testing world: Foundry and Hardhat. We're not here to declare a winner, but to map the trade-offs so you can choose the right tool for your project's unique journey.
Architectural Divergence: Rust-Powered Forge vs. Node.js Flexibility
At its core, this is a debate between two fundamentally different philosophies.
Foundry is built from the ground up in Rust. Its command-line tool, Forge, executes tests written directly in Solidity. This isn't just a stylistic choice—it's a performance revolution. Because there's no JavaScript interpreter in the loop, test execution is often 10x to 100x faster than JS-based frameworks for complex suites. If your project has hundreds of test cases that each deploy multiple contracts and run through lengthy state transitions, that speed difference turns a five-minute test run into a thirty-second one. The development experience is terminal-first. You write your contract code, you write your test code, and you run forge test. The entire mental model is unified around the EVM and Solidity. There is no intermediary language, no serialization layer, no async/await ceremony—just direct interaction with the virtual machine.
Hardhat is a pillar of the JavaScript/TypeScript ecosystem, built on Node.js. It leverages libraries like Ethers.js, Chai, and Mocha that frontend and backend Web2 developers already know intimately. Its power lies in its vast, mature plugin ecosystem. Need to deploy to five testnets with different configurations? There's a plugin. Want a gas report for every test run? There's a plugin. This ecosystem turns Hardhat into a customizable swiss-army knife for the entire development lifecycle, not just testing. The architecture reflects a belief that smart contract development doesn't happen in isolation—it happens inside broader application stacks that speak JavaScript fluently.
Foundry's architecture lets you think in pure EVM terms—write a test in the same language you write the contract, and the tool doesn't get in your way.
The performance gap isn't theoretical. Foundry runs its own EVM implementation called revm, written in Rust, which compiles to machine code. Hardhat runs on Hardhat Network, a JavaScript-based EVM that is fast for a JS implementation but fundamentally constrained by the V8 engine's overhead. For small projects with a few dozen tests, the difference is negligible. For enterprise-grade protocols with thousands of test cases, the divergence becomes a real factor in daily workflow.
Testing Paradigms: Writing Solidity-Native Tests vs. JavaScript/TypeScript Suites
This is where the daily workflow diverges most sharply.
With Foundry, your test file is just another Solidity contract. You import your contract, write a setUp() function to deploy it, and write test functions prefixed with test. Assertions are made with simple require statements or the framework's own assertion library. It feels immediate. The language barrier is zero—you're already writing Solidity. There is no context-switch cost when you need to understand what a test is doing, because the test code and the contract code share the same syntax, the same type system, and the same mental model.
// Inside test/MyContract.t.sol
function testDeposit() public {
myContract.deposit{value: 1 ether}();
assertEq(myContract.balances(address(this)), 1 ether);
}With Hardhat, you write tests in JavaScript or TypeScript using Mocha's describe and it blocks. You connect to your deployed contracts via Ethers.js and use Chai for assertions. The context switches between Solidity and JS are constant but can be powerful for certain tasks, like complex data parsing or integrating with off-chain services in your tests.
// Inside test/MyContract.test.js
it("Should update balance on deposit", async function () {
await myContract.deposit({ value: ethers.utils.parseEther("1") });
const balance = await myContract.balances(owner.address);
expect(balance).to.equal(ethers.utils.parseEther("1"));
});The trade-off here is between unity of language (Foundry) and familiarity with a massive general-purpose language (Hardhat). For a Solidity developer, Foundry's tests often feel more concise and direct. For a full-stack team, Hardhat's tests might be easier to integrate into a broader JavaScript testing suite.
There's another practical dimension: readability of failure messages. Hardhat's Chai integration produces rich, human-readable assertion failures out of the box—"expected 999999 to equal 1000000." Foundry's default assertion messages are more terse, though you can pass custom error strings to assertEq. For teams that spend significant time debugging failing CI pipelines, the richness of failure output matters more than most developers initially expect.
Advanced State Manipulation: Leveraging Foundry Cheatcodes for Complex Scenarios
This is where Foundry truly shines for deep, protocol-level debugging. Its "cheatcodes" are pre-deployed contracts that let you manipulate the EVM's state during tests in ways that are otherwise cumbersome or impossible.
vm.prank(address): Executes the next call as if it came from a specified address. Crucial for testing permissioned functions without deploying mock caller contracts.vm.startPrank(address): The persistent variant—every subsequent call behaves as if it originates from the spoofed address until you callvm.stopPrank(). Useful when testing multi-step workflows that require a specific caller context throughout.vm.warp(timestamp): Sets theblock.timestampto a specific value. Essential for testing time-dependent logic like vesting schedules, governance voting periods, or interest accrual.vm.roll(blockNumber): Sets theblock.number. Perfect for testing forked historical data or simulating the passage of many blocks.vm.store(slot, value): Directly writes to a contract's storage slot. The nuclear option for bypassing access controls in a test to set up a specific state without going through the contract's public interface.vm.deal(address, amount): Instantly funds an address with ETH, removing the need for complex coinbase mining workarounds.vm.expectRevert(): Asserts that the next external call reverts, with optional checks on the revert reason. Clean, declarative error-path testing.
In Hardhat, achieving the same requires more boilerplate. You'd need to use Hardhat Network's JSON-RPC methods like evm_setNextBlockTimestamp or evm_mine, and for storage manipulation, you'd typically have to build a helper function or use a plugin. Hardhat does have hardhat-network-helpers which provides time.increaseTo() and mine() utilities—these are good, but they feel like wrappers around the EVM rather than native extensions of it.
When you need to simulate a hack that involves a flash loan, a price oracle manipulation, and a governance takeover in the same transaction, Foundry's cheatcodes are the difference between a 50-line test and a 500-line test.
This matters most in security auditing and protocol development. When you're trying to reproduce an exploit path that involves impersonating multiple addresses, jumping across time, and manipulating balances in the same transaction sequence, Foundry's cheatcodes compose naturally in a single Solidity function. In Hardhat, the equivalent test often sprawls across multiple beforeEach blocks, helper functions, and manual evm_snapshot/evm_revert calls.
Ecosystem Maturity and Plugin Integration for Deployment Workflows
While Foundry is catching up with forge-deploy and other tools, Hardhat's plugin ecosystem is its undisputed stronghold. This isn't about "bells and whistles"—it's about production-grade workflows that save hours every week.
- Verification: The
@nomiclabs/hardhat-etherscanplugin makes verifying contracts on block explorers a one-command process. With Foundry, you currently rely on separate tools likefoundry-verifyor manual API calls to Etherscan. - Deployment Management: Plugins like
hardhat-deploymanage deployment artifacts, allow for deterministic deployments (via CREATE2 salts), and simplify multi-network rollouts. You can define deployment steps as named functions that run in order, skip already-deployed contracts, and export addresses automatically. This is a lifesaver for complex projects that deploy across Ethereum mainnet, multiple L2s, and testnets. - Gas Reporting & Coverage: Plugins like
hardhat-gas-reporterandsolidity-coverageintegrate directly into your test run, providing actionable metrics without extra setup.hardhat-gas-reportereven shows per-method gas costs in a clean table, which is invaluable during optimization passes. - TypeScript Type Generation:
typechaingenerates TypeScript bindings for your contracts automatically, giving your tests full autocompletion and type safety. Foundry hasforge bindnow, but the TypeChain ecosystem has years of production usage behind it. - Account Management:
hardhat-ledgerandhardhat-mnemonicplugins handle hardware wallet signing and HD wallet derivation for deployment, critical for teams that store mainnet keys on hardware devices.
For a team that values a fully integrated, GUI-friendly dashboard for every aspect of the contract lifecycle, Hardhat's ecosystem can feel like a complete IDE. Foundry, in contrast, feels like a set of precision, high-performance command-line tools. The choice often comes down to: do you want a fully-featured workshop (Hardhat) or a set of razor-sharp, focused instruments (Foundry)?
It's worth noting that Foundry's ecosystem is growing rapidly. Tools like chisel (an interactive Solidity REPL) and cast (a CLI for interacting with contracts) fill gaps that previously required external tooling. The gap is narrowing, but today, Hardhat still leads on breadth of integration.
Native Fuzzing and Invariant Testing Capabilities in Modern Development
Both frameworks now support property-based testing, but their integration differs.
Foundry has fuzz testing baked into its core. Any test function that accepts input arguments automatically becomes a fuzz test—Forge generates random inputs to try and break your assertions. You write function testFuzz_Deposit(uint256 amount) and Forge runs it hundreds of times with random values, automatically shrinking failing inputs down to the minimal reproduction case. Its invariant testing (invariant_* functions) is equally first-class. You define properties that should always hold true—like "total supply equals sum of all balances"—and the fuzzer bombards a suite of contracts with sequences of random calls to try to violate them.
The key power of Foundry's invariant testing is the concept of "call sequences." Forge doesn't just call individual functions with random arguments—it generates random sequences of function calls, interleaved across multiple contracts, and checks invariants after each sequence. This is the kind of testing that catches bugs where two seemingly safe operations, when combined in an unexpected order, produce a broken state. It's the closest thing to automated exploit hunting that exists in a general testing framework.
Hardhat can also perform fuzz testing, but it typically requires integrating an external tool or library. The hardhat-fuzz plugin or using a library like fast-check within your Mocha tests are common paths. The integration, while possible, is not as seamless as Foundry's "just add an argument" approach. Setting up invariant testing in Hardhat often means writing custom test harnesses that deploy contracts, generate call sequences manually, and check invariants in loop—doable, but with significantly more boilerplate.
For a security-focused team, this native fuzzing capability is a major point in Foundry's favor. It lowers the friction to writing comprehensive, property-based tests that catch edge cases a human tester would never imagine. When a fuzz test discovers that your vault contract breaks at type(uint256).max - 1, you'll be glad you wrote it.
Let's compare the core offerings side-by-side:
| Aspect | Foundry | Hardhat |
|---|---|---|
| Language | Rust (Core), Solidity (Tests) | JavaScript/TypeScript (Tests) |
| Execution Speed | Very High (Rust, native EVM) | Moderate (Node.js) |
| State Manipulation | Native Cheatcodes (Solidity API) | Via JSON-RPC methods & plugins |
| Testing Style | Solidity-native, unified | JS/TS, Mocha/Chai/Ethers.js |
| Ecosystem & Plugins | Growing, focused on core | Massive, mature, all-encompassing |
| Fuzz/Invariant Testing | First-class, native integration | Possible, requires plugins/tools |
| Deployment Tooling | forge-deploy, scripts | hardhat-deploy, rich plugin set |
| Contract Verification | External tools or manual | One-command via plugin |
| TypeScript Bindings | forge bind | TypeChain (mature) |
Making the Call: It's About Your Team's Neurology, Not Just Technology
So, how do you choose? We need to look at our team, our project, and the friction points we hate the most.
Choose Foundry if:
- Your team lives and breathes Solidity. The thought of context-switching to JS for tests is a real productivity tax.
- You prioritize raw execution speed above all else. Long test suites are a daily bottleneck and you've already optimized everything else.
- You work on complex DeFi protocols where deep state manipulation and robust fuzz testing are non-negotiable for security audits.
- You prefer a lean, terminal-driven workflow and are comfortable without an all-in-one GUI dashboard.
- You want your tests to serve as documentation—Solidity tests are readable by anyone who can read the contract itself.
Choose Hardhat if:
- Your team is polyglot, with strong JavaScript/TypeScript expertise. The familiarity lowers the onboarding cost for new developers joining the project.
- You need a mature, out-of-the-box workflow for deployment, verification, and reporting today. The plugin ecosystem is a huge accelerator that can't be replicated overnight.
- Your project involves heavy interaction with off-chain services in tests, where JavaScript's flexibility and its npm ecosystem are genuine advantages.
- You value a comprehensive, integrated environment that extends beyond pure testing into deployment management and monitoring.
- Your smart contracts are part of a larger dApp where the frontend and backend are already in JavaScript/TypeScript—shared tooling reduces context-switching across the entire stack.
The right testing framework is the one that disappears—where the friction between your idea and its verification approaches zero, and you stop thinking about the tool entirely.
Use both. There's no rule that says you must pick one forever. Some teams write unit tests and fuzz tests in Foundry for speed and security, then use Hardhat for deployment scripts, verification, and integration testing against external services. The tools don't compete—they complement.
Start by cloning a boilerplate for each. Write the same test suite in both Foundry and Hardhat. Spend a day with each. The one that feels less like a tool and more like an extension of your thought process—that's your winner. Let's build.