Web3 Testing FAQ: Your Questions Answered
Diving into the world of Web3 testing brings up a lot of questions. We've gathered the most common ones we hear from QA engineers and developers and answered them right here. Think of this as your quick-start guide to the tricky parts.
Testing Strategy & Methodology
Q: What's the difference between a unit, integration, and E2E test in Web3?
Great question! They represent different layers of your testing strategy.
- Unit Tests are the smallest and most common. You test a single function in a single contract in isolation. For example, "Does the
transfer()function correctly update balances?" - Integration Tests check how multiple smart contracts work together. You deploy several contracts and test a workflow that involves them calling each other. For example, "Can a user mint an NFT from our
NFT.solcontract and then stake it in ourStaking.solcontract?" - End-to-End (E2E) Tests are the final layer. You test the entire application from the user's perspective, including the front-end. Using a tool like Playwright, you'd write a script to connect a wallet, click buttons in the UI, approve transactions, and verify the outcome.
Q: How much testing is enough? What's a good test-to-code ratio?
In Web3, there's no such thing as "too much testing." Unlike traditional software where you might aim for 70-80% code coverage, Web3 projects often target 100% because the cost of bugs is so high.
A good rule of thumb:
- 2-3 test files for every 1 contract file
- 5-10 test cases for every public function
- Additional edge case and attack vector tests
Remember: You're not just testing functionality—you're testing security, gas efficiency, and economic models.
Q: Should I test on mainnet forks or just local networks?
Both have their place in a comprehensive testing strategy:
Local Networks (Hardhat's built-in):
- Fast and free
- Perfect for unit and integration tests
- Great for development iteration
- Use for 95% of your daily testing
Mainnet Forks:
- Test against real contract state
- Verify integrations with live protocols
- Test gas costs under real conditions
- Use for final validation before deployment
The typical workflow: Develop and test locally, then run critical tests on mainnet forks before deploying.
Networks & Environments
Q: Why can't I just test everything on a public testnet like Sepolia?
You can, but you'll be moving at a snail's pace! 🐌
- Testnets like Sepolia are crucial for your final pre-deployment checks. They mimic the real-world conditions of the mainnet.
- Local Blockchains (like the one built into Hardhat) are for your day-to-day development and testing. They are instant, cost nothing to use, and give you powerful debugging tools.
The typical workflow is to run hundreds of tests on your local network for every one test you run on a public testnet.
Q: How do I test functions that depend on time?
This is a classic Web3 problem! If you have a function like unlockFunds() that can only be called after a year, you don't want to wait a year to test it.
Luckily, local frameworks like Hardhat give you special "cheat codes." You can use commands like:
// Fast-forward time by 1 year
await time.increase(365 * 24 * 60 * 60);
// Jump to a specific timestamp
await time.increaseTo(targetTimestamp);
// Get the latest block timestamp
const latestTime = await time.latest();
This lets you test time-locked features in seconds rather than waiting for real time to pass.
Q: What's the difference between testing on different networks (Ethereum, Polygon, Arbitrum)?
Each network has subtle differences that can affect your contracts:
Gas Costs:
- Ethereum mainnet: High gas, expensive testing
- Polygon: Very low gas, cheap transactions
- Arbitrum/Optimism: Medium gas, L2 benefits
Block Times:
- Ethereum: ~12 seconds
- Polygon: ~2 seconds
- Arbitrum: Variable based on activity
Testing Strategy: Develop on a local network, test gas optimization on the target network's testnet, and consider cross-chain compatibility if deploying to multiple networks.
Security & Audits
Q: What is a security audit, and do I still need one if I write good tests?
Yes, a million times yes! This is critically important.
- Your Tests are for verifying that your contract does what you expect it to do. They check for known logic and prevent regressions.
- A Security Audit is performed by third-party experts who are trained to think like hackers. Their job is to find the unexpected—the complex economic exploits and unknown vulnerabilities that your tests would never think to look for.
Think of it this way: Your tests make sure the windows and doors are locked. An audit makes sure someone can't just knock down the wall. You need both.
Q: How do I test for common vulnerabilities like reentrancy attacks?
Testing for vulnerabilities requires thinking like an attacker. Here's how to test for reentrancy:
// Create a malicious contract that re-enters your function
const MaliciousContract = await ethers.getContractFactory("AttackerContract");
const attacker = await MaliciousContract.deploy(yourContract.address);
// Test that your contract prevents the attack
await expect(
attacker.attemptReentrancy()
).to.be.revertedWith("ReentrancyGuard: reentrant call");
Other vulnerabilities to test:
- Integer overflow/underflow: Test with extreme values
- Access control: Verify only authorized users can call functions
- Front-running: Test if transaction order affects outcomes
- Flash loan attacks: Simulate large, temporary capital injections
Q: Should I use formal verification tools?
Formal verification tools like Certora or K Framework can mathematically prove contract properties, but they're not for everyone:
Use formal verification if:
- Your contract handles large amounts of value (>$10M)
- You have complex mathematical invariants
- You're building critical DeFi infrastructure
Stick with traditional testing if:
- You're building simpler contracts
- You have limited time/budget
- Your team lacks formal verification expertise
Most projects get excellent security with comprehensive unit tests, integration tests, and professional audits.
Code Quality & Coverage
Q: What does "code coverage" mean, and is 100% always the goal?
Code coverage tells you what percentage of your smart contract's code was executed by your test suite. A tool like solidity-coverage makes this easy to measure.
While 100% coverage is an excellent goal, it doesn't automatically mean your contract is secure. It's possible to "cover" a line of code without actually testing its logic properly. For example, you could call a function but never check if it produced the correct result.
The Verdict: Aim for 100% coverage as a baseline, but remember that the quality and thoughtfulness of your tests are what truly matters.
Q: How do I measure and improve test coverage?
Install and run the coverage tool:
npm install --save-dev solidity-coverage
npx hardhat coverage
This generates a detailed report showing:
- Which lines were executed
- Which branches were taken
- Which functions were called
Improving coverage:
- Test all function branches: Every
if/elsestatement should have tests for both paths - Test edge cases: Empty arrays, zero values, maximum values
- Test error conditions: Functions should fail gracefully with proper error messages
- Test state changes: Verify that contract state is updated correctly
Q: My test suite is getting slow. How can I speed it up?
As your project grows, your test suite can start to drag. Here are proven optimization strategies:
1. Use Test Fixtures:
const deployFixture = async () => {
// Deploy contracts once
const contract = await Contract.deploy();
return { contract };
};
// Reuse the fixture in multiple tests
beforeEach(async () => {
({ contract } = await loadFixture(deployFixture));
});
2. Run Tests in Parallel:
npx hardhat test --parallel
3. Optimize Gas Usage:
- Use
staticcallfor read-only operations - Batch multiple operations into single transactions
- Consider using create2 for deterministic addresses
4. Consider Foundry: For very large projects, Foundry (written in Rust) can be 10-100x faster than JavaScript-based frameworks.
Tools & Frameworks
Q: Hardhat vs. Foundry vs. Truffle - which should I choose?
Each has its strengths:
Hardhat (JavaScript/TypeScript):
- ✅ Great for teams with JavaScript experience
- ✅ Excellent debugging and error messages
- ✅ Large ecosystem of plugins
- ❌ Slower test execution
Foundry (Solidity):
- ✅ Extremely fast test execution
- ✅ Write tests in Solidity itself
- ✅ Advanced testing features (fuzzing, invariant testing)
- ❌ Steeper learning curve
Truffle (JavaScript):
- ✅ Mature and stable
- ✅ Good for simple projects
- ❌ Less active development
- ❌ Fewer modern features
Recommendation: Start with Hardhat for most projects. Consider Foundry if speed becomes critical or you want advanced testing features.
Q: What's the best way to test frontend integration?
Testing the full stack (frontend + smart contracts) requires a different approach:
Option 1: Playwright + Local Network
// In your E2E test
await page.goto('localhost:3000');
await page.click('[data-testid="connect-wallet"]');
await page.click('[data-testid="mint-nft"]');
// Verify the transaction succeeded
Option 2: React Testing Library + Contract Mocks
// Mock the contract calls for faster unit tests
const mockContract = {
mint: jest.fn().mockResolvedValue({ hash: '0x123...' })
};
Best Practice: Use both approaches—mock contracts for fast unit tests, real contracts for E2E tests.
Common Pitfalls & Gotchas
Q: Why do my tests pass locally but fail on CI/CD?
Common causes and solutions:
1. Inconsistent Node.js versions:
// package.json - pin exact versions
{
"engines": {
"node": "18.17.0",
"npm": "9.6.7"
}
}
2. Race conditions in parallel tests:
// Use isolated test fixtures
beforeEach(async () => {
await deployFresh();
});
3. Time-dependent tests:
// Don't rely on real time - use blockchain time
const blockTime = await time.latest();
await time.increaseTo(blockTime + 3600);
Q: How do I test contracts that interact with oracles?
Oracle testing requires mocking external data sources:
// Create a mock oracle
const MockOracle = await ethers.getContractFactory("MockChainlinkOracle");
const oracle = await MockOracle.deploy();
// Set test data
await oracle.setPrice(ethers.utils.parseEther("2000")); // $2000
// Test your contract's response to oracle data
await expect(yourContract.processPrice()).to.not.be.reverted;
Advanced testing:
- Test with stale data (old timestamps)
- Test with extreme price movements
- Test oracle failure scenarios
Q: How do I handle private key management in tests?
Never use real private keys in tests! Use these safe approaches:
// Hardhat provides test accounts automatically
const [owner, user1, user2] = await ethers.getSigners();
// Or generate deterministic test keys
const testPrivateKey = "0x" + "0".repeat(63) + "1";
const wallet = new ethers.Wallet(testPrivateKey, ethers.provider);
For CI/CD environments:
- Use GitHub Secrets or similar for testnet keys
- Never commit private keys to version control
- Use separate keys for each environment
Performance & Gas Optimization
Q: How do I test gas consumption and optimize costs?
Gas testing is crucial for user experience:
it("should use reasonable gas for minting", async () => {
const tx = await contract.mint(user.address);
const receipt = await tx.wait();
// Assert gas usage is below threshold
expect(receipt.gasUsed).to.be.below(100000);
});
Gas optimization strategies:
- Use
uint256instead of smaller uints when possible - Pack struct variables efficiently
- Use
calldatainstead ofmemoryfor external function parameters - Consider assembly for gas-critical functions
Q: How do I test under different network congestion scenarios?
Simulate network conditions by adjusting gas prices:
// Test with high gas prices (network congestion)
const highGasPrice = ethers.utils.parseUnits("100", "gwei");
const tx = await contract.connect(user).transfer(recipient, amount, {
gasPrice: highGasPrice
});
// Verify the transaction still works economically
expect(tx.gasPrice).to.equal(highGasPrice);
This helps ensure your contract remains usable even during network stress.
Advanced Topics
Q: How do I test upgradeable contracts?
Upgradeable contracts require special testing considerations:
// Test the upgrade process itself
const V1 = await ethers.getContractFactory("ContractV1");
const V2 = await ethers.getContractFactory("ContractV2");
const proxy = await upgrades.deployProxy(V1, [initArgs]);
await upgrades.upgradeProxy(proxy.address, V2);
// Verify state is preserved after upgrade
expect(await proxy.someStateVariable()).to.equal(expectedValue);
Key testing areas:
- State preservation across upgrades
- Storage layout compatibility
- Access control for upgrade functions
- Migration scripts for data transformation
Q: How do I test cross-chain functionality?
Cross-chain testing is complex but manageable:
// Set up multiple network connections
const ethereumProvider = new ethers.providers.JsonRpcProvider(ethereumRPC);
const polygonProvider = new ethers.providers.JsonRpcProvider(polygonRPC);
// Test bridge functionality
await bridgeContract.connect(ethereumProvider).deposit(amount);
// Simulate cross-chain message
await bridgeContract.connect(polygonProvider).processDeposit(proof);
Consider using:
- Hardhat's multi-network configuration
- Cross-chain testing frameworks like LayerZero's tools
- Mainnet forking for realistic cross-chain state
This FAQ covers the most common questions we encounter in Web3 testing. As the ecosystem evolves, we'll continue updating this resource with new insights and solutions.
Have a question not covered here? Feel free to reach out—your question might become the next addition to this FAQ!