Why Test Smart Contracts
Smart contracts are immutable once deployed: unlike a web server you can patch and redeploy in minutes, a bug in a live contract can permanently lock or drain user funds, and there is no undo button. Testing is therefore not optional quality assurance but a core part of writing correct contracts. Hardhat lets you run your contracts against a local, instant, in-process Ethereum network so you can exercise every function, revert path, and edge case thousands of times before spending real gas on a public network.
Cricket analogy: It mirrors a DRS review before a batter finally walks off — once the umpire's decision is locked and the player leaves the field, it can't be reversed, so teams verify every marginal LBW call before it becomes final.
The Hardhat Testing Stack
Hardhat's default testing stack combines the Mocha test runner, the Chai assertion library, and the ethers.js library for interacting with contracts. The @nomicfoundation/hardhat-chai-matchers plugin adds Solidity-aware assertions such as expect(...).to.be.revertedWith('Not owner'), .to.emit(contract, 'Transfer').withArgs(...), and .to.changeEtherBalance(...). You deploy a contract inside a test with ethers.getContractFactory and factory.deploy(), then call its functions and assert on return values, emitted events, reverts, and resulting on-chain state, all against Hardhat's fast in-memory network.
Cricket analogy: It's like a fielding side using specialist tools together, Hawk-Eye for trajectory, Snickometer for edges, Hot Spot for contact, where each assertion matcher checks one specific kind of outcome precisely.
const { expect } = require("chai");
const { ethers } = require("hardhat");
const { loadFixture } = require("@nomicfoundation/hardhat-network-helpers");
describe("Vault", function () {
async function deployVaultFixture() {
const [owner, attacker] = await ethers.getSigners();
const Vault = await ethers.getContractFactory("Vault");
const vault = await Vault.deploy();
return { vault, owner, attacker };
}
it("accepts deposits and emits an event", async function () {
const { vault, owner } = await loadFixture(deployVaultFixture);
await expect(vault.deposit({ value: ethers.parseEther("1.0") }))
.to.emit(vault, "Deposit")
.withArgs(owner.address, ethers.parseEther("1.0"));
expect(await vault.balanceOf(owner.address)).to.equal(ethers.parseEther("1.0"));
});
it("blocks withdrawals from non-owners", async function () {
const { vault, attacker } = await loadFixture(deployVaultFixture);
await expect(vault.connect(attacker).withdrawAll())
.to.be.revertedWith("Not owner");
});
});Fixtures and Test Isolation
Re-deploying contracts and re-seeding state at the start of every it block is slow and error-prone. Hardhat's loadFixture helper solves this: the first time it runs a fixture function it executes the setup and takes a snapshot of the blockchain state; every subsequent call instantly reverts the network to that snapshot instead of re-running the code. This keeps tests fast and, crucially, fully isolated, so no test can leak state into another and a deposit made in one test never accidentally affects the balance asserted in the next.
Cricket analogy: It's like rolling and re-marking the pitch to identical conditions before each innings, so one team's play never leaves the surface altered for the next, giving a clean, repeatable baseline.
loadFixture is provided by @nomicfoundation/hardhat-network-helpers. Only use it for setup that does not depend on the current block state, since it always reverts to the first snapshot. Its speed comes from the evm_snapshot / evm_revert JSON-RPC methods on Hardhat's local node.
Coverage and Gas Reporting
Two plugins turn passing tests into measurable quality signals. solidity-coverage instruments your contracts and reports the percentage of lines, branches, and functions your tests actually execute, exposing untested revert paths and unreached branches that are exactly where bugs hide. hardhat-gas-reporter prints the gas cost of each function call and deployment as your suite runs, so you can catch expensive regressions and compare optimizations. High coverage does not prove correctness, since you can execute a line without asserting anything meaningful about it, but low coverage reliably proves you have blind spots.
Cricket analogy: It's like a wagon-wheel chart showing which parts of the ground a batter has scored in, where gaps reveal untested shot regions, though hitting an area is not the same as scoring well there.
Do not treat a green test suite or 100% coverage as proof of correctness. Coverage only measures which code ran, not whether your assertions checked the right thing. Always assert on concrete outcomes, events, reverts, and state, and deliberately test malicious and edge-case inputs, not just the happy path.
- Contracts are immutable once deployed, so thorough testing before deployment is essential, not optional.
- Hardhat's stack is Mocha (runner) + Chai (assertions) + ethers.js (contract interaction) on a fast in-process network.
- hardhat-chai-matchers adds Solidity-aware assertions like revertedWith, emit/withArgs, and changeEtherBalance.
- loadFixture snapshots setup state and reverts to it between tests, keeping them fast and isolated.
- connect(signer) sends transactions from a specific account, letting you simulate attackers and access-control checks.
- solidity-coverage measures line/branch/function coverage; low coverage proves blind spots, high coverage does not prove correctness.
- hardhat-gas-reporter surfaces per-function and deployment gas costs so you can catch regressions and validate optimizations.
Practice what you learned
1. Which assertion checks that a transaction reverts with a specific reason string?
2. What is the main benefit of loadFixture over redeploying in beforeEach?
3. What does solidity-coverage measure?
4. Which statement about test coverage is accurate?
5. In a Hardhat test, how do you send a transaction from an account other than the default signer?
Was this page helpful?
You May Also Like
Setting Up Remix and Hardhat
Learn the two most common Solidity development environments: the browser-based Remix IDE for quick experiments and the Node.js-based Hardhat framework for professional project workflows.
Deploying to Testnets and Mainnet
A practical guide to deploying Solidity contracts with Hardhat: configuring networks and keys, understanding gas and nonces, waiting for confirmations, and verifying source on Etherscan.
Auditing Smart Contracts
What a smart contract audit is and is not: the common vulnerability classes auditors hunt, the static, symbolic, and fuzzing tools that assist them, and why manual review plus layered defenses matter more than any single report.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics