By Fatskills Exam Guides Team — the exam nerds behind 28,500+ quizzes and 2.1M practice questions across 500+ global exams.
Testing and debugging smart contracts is the practice of automatically verifying that every function in a Solidity program behaves exactly as intended before you lock it on‑chain. Because blockchain code is immutable and every mistake costs real gas (and sometimes user funds), a solid test suite—combined with live‑debug tools like console.log and Tenderly—lets you catch bugs, edge‑cases, and security flaws early.
console.log
Real‑world example: Imagine a Uniswap V3 swap contract. If the price‑oracle logic is off by just one basis point, traders could lose thousands of dollars in a single transaction. Unit tests that simulate swaps across many price ranges, plus a Tenderly “simulation” of the swap on a forked mainnet, guarantee the contract will settle correctly when it goes live.
describe("MyToken", function () { it("mints the correct amount", async () => { const [owner] = await ethers.getSigners(); const MyToken = await ethers.getContractFactory("MyToken"); const token = await MyToken.deploy(); await token.mint(owner.address, 1000); expect(await token.balanceOf(owner.address)).to.equal(1000); }); }); ```
function add(uint256 a, uint256 b) external pure returns (uint256) { uint256 sum = a + b; console.log("add(): a=%s b=%s sum=%s", a, b, sum); return sum; } ```
Tenderly Simulation: A cloud service that replays a transaction on a forked state, showing gas usage, internal calls, and state changes. You can upload a Hardhat test transaction and see a visual trace.
assert vs require: assert is for internal invariants (should never fail); require validates user‑provided data and reverts with a custom error message. Using assert for user checks wastes gas and can trigger a panic error.
assert
require
Fuzz Testing (Foundry): Automatically generates random inputs to a function and checks that invariants hold (e.g., balances never go negative). ```solidity // test/Fuzz.t.sol contract FuzzTest is Test { MyToken token; function setUp() public { token = new MyToken(); }
function testTransferFuzz(uint256 amount, address from, address to) public { vm.assume(amount < type(uint256).max / 2); token.mint(from, amount); token.transfer(to, amount); assertEq(token.balanceOf(to), amount); } } ```
Coverage Report: A metric (usually % of lines/branches) that tells you how much of your contract code is exercised by tests. Hardhat’s hardhat-coverage plugin produces an HTML report.
hardhat-coverage
evm_snapshot / evm_revert: Hardhat/Ethers methods that snapshot the blockchain state before a test and revert after, keeping each test isolated and fast. js await network.provider.send("evm_snapshot", []); // …run test actions… await network.provider.send("evm_revert", [snapshotId]);
evm_snapshot
evm_revert
js await network.provider.send("evm_snapshot", []); // …run test actions… await network.provider.send("evm_revert", [snapshotId]);
delegatecall Pitfall: When a proxy contract forwards calls via delegatecall, the storage layout of the implementation must exactly match the proxy. Tests must deploy the proxy and implementation together and assert that state variables line up.
delegatecall
Gas‑Profiler (hardhat-gas-reporter): Shows per‑function gas cost in your test output, helping you spot expensive loops or unnecessary storage writes.
hardhat-gas-reporter
Static Analyzer (slither): Not a test but a pre‑run scanner that flags reentrancy, unchecked low‑level calls, and other vulnerabilities. Run it on your repo before you write tests to catch low‑hanging fruit.
slither
npm init -y && npm install --save-dev hardhat @nomicfoundation/hardhat-chai-matchers ethers
npx hardhat
npm install --save-dev hardhat-ethers hardhat-console hardhat-gas-reporter
import "hardhat/console.sol";
test/
beforeEach
await ethers.getContractFactory
deploy
npx hardhat test
npx hardhat coverage
coverage/index.html
Mistake: Using tx.origin for access control. Correction: Always use msg.sender. tx.origin can be spoofed through a malicious contract, letting attackers bypass restrictions.
tx.origin
msg.sender
Mistake: Forgetting to reset the blockchain state between tests, causing state bleed‑over. Correction: Use await network.provider.send("evm_snapshot") in a beforeEach hook and evm_revert in an afterEach hook, or rely on Hardhat’s automatic fixture reset.
await network.provider.send("evm_snapshot")
afterEach
Mistake: Writing tests that only cover the “happy path” (e.g., successful transfers) and ignoring failure branches. Correction: Add expect(...).to.be.revertedWith("...") for every require/revert path, and use fuzzing to explore edge cases like zero amounts, max uint256, and address zero.
expect(...).to.be.revertedWith("...")
revert
Mistake: Assuming a proxy’s storage layout is correct without testing. Correction: Deploy the proxy + implementation in a test, call a setter on the implementation via the proxy, then read the value directly from the proxy to confirm the slot mapping.
Mistake: Ignoring gas‑report warnings and shipping a contract with an O(n) loop that could hit block‑gas limits. Correction: Run hardhat-gas-reporter each test run; refactor any function that exceeds ~50 k gas per iteration into a batched or off‑chain process.
“Explain the difference between call and delegatecall and when you’d use each.” Interviewers look for understanding of context switching: call executes code in the callee’s storage, while delegatecall runs the callee’s code in the caller’s storage—the backbone of upgradeable proxy patterns.
call
“How would you test that a newly added ERC‑20 permit function (EIP‑2612) works correctly?” Expect a demonstration of signing an off‑chain EIP‑712 message, feeding the signature to the contract in a test, and asserting that the allowance is set without a separate approve transaction.
permit
approve
“What are the pros and cons of using Tenderly vs. a local Hardhat fork for debugging complex DeFi interactions?” Key points: Tenderly gives a UI, real‑time gas estimates, and persistent transaction history; a local fork is faster, fully scriptable, and can be integrated into CI pipelines.
“Describe how you’d detect a reentrancy bug using unit tests.” Look for a test that deploys a malicious attacker contract, calls the vulnerable function, and asserts that the attacker’s balance does not increase beyond the expected amount.
Scenario: A contract uses tx.origin to restrict minting to the contract owner. Answer: Dangerous – an attacker can trick a user into calling a malicious contract that then calls the mint function, and tx.origin will still be the user’s address, bypassing the restriction.
Scenario: Your unit test passes, but a Tenderly simulation shows a revert due to “insufficient funds”. Answer: The test likely used a mocked balance or a hard‑coded msg.sender with unlimited ether; you need to fund the test address with realistic ether (e.g., await ethers.provider.sendTransaction({to: addr, value: ethers.utils.parseEther("10")})).
await ethers.provider.sendTransaction({to: addr, value: ethers.utils.parseEther("10")})
Scenario: You added console.log statements to a contract, but they never appear in the Hardhat test output. Answer: console.log only works when the contract is compiled with the Hardhat Solidity compiler (hardhat compile) and the test is run with npx hardhat test; ensure you’re not using a different compiler (e.g., Foundry) or a production build.
hardhat compile
ReentrancyGuard
vm.assume
SafeMath
tokensReceived
Join 4M+ learners. Unlock unlimited quizzes, wrong-answer tracking, flashcards + reminders, study guides, and 1-on-1 challenges.