Why 52 Tests?
When building an NFT marketplace, I didn't set out to write 52 tests. But every time I thought "what if someone tries to...", I added another test. And it turns out there are a lot of ways to break a smart contract.
The Security Mindset
Smart contract development requires a different mindset than traditional web development. Code is immutable and controls real value. A bug in production isn't just embarrassing - it can cost users money.
This changes how you think about testing. It's not enough to test happy paths. You need to actively try to break your own code.
Key Lessons Learned
1. Reentrancy is Everywhere
The classic attack pattern. Even when you think you're safe, check again.
Bad:
function withdraw() public {
uint amount = balances[msg.sender];
msg.sender.call{value: amount}(""); // Reentrancy vulnerability!
balances[msg.sender] = 0;
}
Good:
function withdraw() public {
uint amount = balances[msg.sender];
balances[msg.sender] = 0; // Update state FIRST
msg.sender.call{value: amount}("");
}
2. Access Control Matters
Don't trust that only the owner will call owner-only functions. Test that regular users CAN'T call them.
function testNonOwnerCannotMint() public {
vm.prank(user1); // Impersonate user1
vm.expectRevert(); // Should fail
nft.mint(user1, "ipfs://token");
}
3. Gas Optimization vs Security
Sometimes the most gas-efficient code isn't the most secure. Choose security first, optimize second.
4. Integer Overflow/Underflow
Even with Solidity 0.8+, be careful with unchecked blocks. Test boundary conditions.
5. External Calls Can Fail
Never assume external calls succeed. Always check return values and handle failures gracefully.
Testing Strategy
My testing pyramid for smart contracts:
- Unit tests (60%): Every function, every edge case
- Integration tests (30%): Complete user flows (mint → list → buy → transfer)
- Security tests (10%): Attack scenarios (reentrancy, unauthorized access, etc.)
Tools That Helped
- Hardhat: Development environment and testing framework
- Foundry: Fast tests and fuzzing
- OpenZeppelin: Battle-tested contract libraries
- Slither: Static analysis tool for finding vulnerabilities
The Result
52 tests covering:
- All functions (mint, list, buy, transfer, cancel)
- Edge cases (minting 0 tokens, listing twice, buying unlisted NFT)
- Access control (non-owner trying owner functions)
- Event emissions (all events properly emitted)
- Gas usage (optimization verification)
- Reentrancy attacks
- Integer overflow/underflow
- State consistency
Key Takeaway
Writing comprehensive tests taught me more about Solidity than any tutorial. When you're actively trying to break your own code, you discover edge cases and security patterns that aren't obvious from documentation.
Testing isn't about hitting coverage numbers - it's about proving your code is safe to handle real value.
The full test suite is on GitHub if you want to see the details.