Testing Apps
Learn best practices for testing applications on Paxeer Network
Overview
Testing applications on Paxeer Network follows the same principles as Ethereum development. This guide covers best practices specific to building reliable applications on Paxeer Network.
For most tests, you don't need Paxeer-specific features. Use your development stack's built-in testing tools for faster iteration.
Testing Strategy
Unit Tests (Local)
Test individual functions with framework's local network
Tools: Hardhat Network, Anvil (Foundry), Ganache
Speed: ⚡ Fastest
When: 90% of your tests
Integration Tests (Local Fork)
Test interactions with deployed contracts
Tools: Hardhat forking, Foundry forking
Speed: ⚡ Fast
When: Testing with existing protocols
Testnet Tests
Test on live network with real conditions
Network: Paxeer Testnet (if available)
Speed: 🐌 Slower
When: Final validation before mainnet
Mainnet
Deploy to production
Network: Paxeer Network (Chain ID: 125)
When: After thorough testing
Unit Testing
Hardhat Example
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("MyContract", function () {
let contract;
let owner;
let addr1;
let addr2;
beforeEach(async function () {
[owner, addr1, addr2] = await ethers.getSigners();
const MyContract = await ethers.getContractFactory("MyContract");
contract = await MyContract.deploy();
await contract.waitForDeployment();
});
describe("Deployment", function () {
it("Should set the right owner", async function () {
expect(await contract.owner()).to.equal(owner.address);
});
it("Should start with zero value", async function () {
expect(await contract.getValue()).to.equal(0);
});
});
describe("Transactions", function () {
it("Should update value", async function () {
await contract.setValue(42);
expect(await contract.getValue()).to.equal(42);
});
it("Should emit ValueChanged event", async function () {
await expect(contract.setValue(42))
.to.emit(contract, "ValueChanged")
.withArgs(42);
});
it("Should revert when unauthorized", async function () {
await expect(
contract.connect(addr1).adminFunction()
).to.be.revertedWith("Not authorized");
});
});
describe("Edge Cases", function () {
it("Should handle zero value", async function () {
await contract.setValue(0);
expect(await contract.getValue()).to.equal(0);
});
it("Should handle max uint256", async function () {
const maxUint = ethers.MaxUint256;
await contract.setValue(maxUint);
expect(await contract.getValue()).to.equal(maxUint);
});
});
});Foundry Example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/MyContract.sol";
contract MyContractTest is Test {
MyContract public myContract;
address owner = address(1);
address user = address(2);
function setUp() public {
vm.prank(owner);
myContract = new MyContract();
}
function testSetValue() public {
myContract.setValue(42);
assertEq(myContract.getValue(), 42);
}
function testEventEmission() public {
vm.expectEmit(true, true, true, true);
emit ValueChanged(42);
myContract.setValue(42);
}
function testUnauthorized() public {
vm.prank(user);
vm.expectRevert("Not authorized");
myContract.adminFunction();
}
function testFuzz_setValue(uint256 value) public {
myContract.setValue(value);
assertEq(myContract.getValue(), value);
}
}Integration Testing
Testing with Mainnet Fork
Fork Paxeer Network mainnet to test against real deployed contracts:
module.exports = {
networks: {
hardhat: {
forking: {
url: "https://public-rpc.paxeer.app/rpc",
blockNumber: 1000000, // Optional: pin to block
},
},
},
};describe("PaxDex Integration", function () {
it("Should swap tokens", async function () {
const vault = await ethers.getContractAt(
"PaxDexVault",
"0x49B0f9a0554da1A7243A9C8ac5B45245A66D90ff"
);
// Test with real contract
const price = await vault.getPrice(wbtcAddress);
expect(price).to.be.gt(0);
});
});# Run tests with fork
forge test --fork-url https://public-rpc.paxeer.app/rpccontract IntegrationTest is Test {
function testPaxDexSwap() public {
// Fork mainnet
vm.createSelectFork("https://public-rpc.paxeer.app/rpc");
// Interact with real contracts
PaxDexVault vault = PaxDexVault(
0x49B0f9a0554da1A7243A9C8ac5B45245A66D90ff
);
uint256 price = vault.getPrice(wbtcAddress);
assertGt(price, 0);
}
}Gas Testing
Measure and Optimize Gas Usage
describe("Gas Optimization", function () {
it("Should track gas usage", async function () {
const tx = await contract.setValue(42);
const receipt = await tx.wait();
console.log("Gas used:", receipt.gasUsed.toString());
// Assert gas usage is within expected range
expect(receipt.gasUsed).to.be.lt(50000);
});
it("Should compare optimized vs unoptimized", async function () {
// Test optimized function
const tx1 = await contract.optimizedFunction();
const receipt1 = await tx1.wait();
// Test unoptimized function
const tx2 = await contract.unoptimizedFunction();
const receipt2 = await tx2.wait();
console.log("Optimized gas:", receipt1.gasUsed.toString());
console.log("Unoptimized gas:", receipt2.gasUsed.toString());
expect(receipt1.gasUsed).to.be.lt(receipt2.gasUsed);
});
});Foundry Gas Snapshots
# Create gas snapshot
forge snapshot
# Compare with previous
forge snapshot --diffcontract GasTest is Test {
function testGas_transfer() public {
token.transfer(user, 100 ether);
}
function testGas_batchTransfer() public {
address[] memory recipients = new address[](10);
// ... test batch operation
}
}Testing Best Practices
Testing Multi-Contract Interactions
describe("Token Vault Integration", function () {
let token, vault;
let owner, user;
beforeEach(async function () {
[owner, user] = await ethers.getSigners();
// Deploy token
const Token = await ethers.getContractFactory("MyToken");
token = await Token.deploy();
// Deploy vault
const Vault = await ethers.getContractFactory("Vault");
vault = await Vault.deploy(await token.getAddress());
// Setup: give user some tokens
await token.transfer(user.address, ethers.parseEther("1000"));
});
it("Should deposit and withdraw", async function () {
const amount = ethers.parseEther("100");
// User approves vault
await token.connect(user).approve(await vault.getAddress(), amount);
// User deposits
await vault.connect(user).deposit(amount);
expect(await vault.balances(user.address)).to.equal(amount);
expect(await token.balanceOf(user.address)).to.equal(
ethers.parseEther("900")
);
// User withdraws
await vault.connect(user).withdraw(amount);
expect(await vault.balances(user.address)).to.equal(0);
expect(await token.balanceOf(user.address)).to.equal(
ethers.parseEther("1000")
);
});
});Continuous Integration
GitHub Actions Example
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
- name: Run tests
run: forge test -vvv
- name: Check coverage
run: forge coverageMock Contracts for Testing
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MockERC20 is ERC20 {
constructor() ERC20("Mock Token", "MOCK") {
_mint(msg.sender, 1000000 * 10 ** decimals());
}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
}Testing Checklist
Before deploying to Paxeer Network mainnet:
- All unit tests passing
- Integration tests passing
- Gas costs optimized
- Test coverage > 90%
- Edge cases tested
- Events tested
- Access control tested
- Reentrancy protection verified
- Integer overflow scenarios tested
- Failed transaction scenarios handled
- Tested with mainnet fork
- Security review completed
- Documentation updated
Testing Tools & Resources
Hardhat
Hardhat testing guide
Foundry
Foundry testing guide
Waffle
Ethereum smart contract testing library
Chai Matchers
Ethereum-specific assertions
Advanced Testing Patterns
Snapshot Testing
describe("State Snapshots", function () {
let snapshotId;
beforeEach(async function () {
snapshotId = await ethers.provider.send("evm_snapshot");
});
afterEach(async function () {
await ethers.provider.send("evm_revert", [snapshotId]);
});
it("Should test with clean state", async function () {
// Each test starts with fresh state
await contract.setValue(42);
expect(await contract.getValue()).to.equal(42);
});
});Testing Reverts
describe("Revert Scenarios", function () {
it("Should revert with message", async function () {
await expect(
contract.connect(user).adminOnly()
).to.be.revertedWith("Only admin");
});
it("Should revert with custom error", async function () {
await expect(
contract.invalidOperation()
).to.be.revertedWithCustomError(contract, "InvalidOperation");
});
it("Should revert with panic code", async function () {
await expect(
contract.divideByZero()
).to.be.revertedWithPanic(0x12); // Division by zero
});
});Testing Gas Usage
describe("Gas Optimization", function () {
it("Should use less gas than limit", async function () {
const tx = await contract.optimizedFunction();
const receipt = await tx.wait();
console.log("Gas used:", receipt.gasUsed.toString());
expect(receipt.gasUsed).to.be.lt(100000);
});
it("Should compare gas between implementations", async function () {
const tx1 = await contract.methodA();
const receipt1 = await tx1.wait();
const tx2 = await contract.methodB();
const receipt2 = await tx2.wait();
console.log("Method A gas:", receipt1.gasUsed.toString());
console.log("Method B gas:", receipt2.gasUsed.toString());
});
});Load Testing
Test your contract under load:
describe("Load Testing", function () {
it("Should handle batch operations", async function () {
const numOperations = 100;
const promises = [];
for (let i = 0; i < numOperations; i++) {
promises.push(contract.setValue(i));
}
await Promise.all(promises);
// Verify all operations succeeded
expect(await contract.getValue()).to.equal(numOperations - 1);
});
it("Should handle large arrays", async function () {
const largeArray = Array(1000).fill(0).map((_, i) => i);
await contract.processBatch(largeArray);
});
});Security Testing
CI/CD Integration
Automated Testing Pipeline
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npx hardhat test
- name: Check coverage
run: npx hardhat coverage
- name: Upload coverage
uses: codecov/codecov-action@v3Debugging Tests
Enable Verbose Logging
# Hardhat
npx hardhat test --verbose
# Foundry
forge test -vvvvUse Console.log in Solidity
import "hardhat/console.sol";
contract Debug {
function testFunction(uint256 x) external {
console.log("Input value:", x);
uint256 result = x * 2;
console.log("Result:", result);
}
}Hardhat Debugging
const { ethers } = require("hardhat");
it("Should debug transaction", async function () {
const tx = await contract.setValue(42);
const receipt = await tx.wait();
console.log("Transaction hash:", receipt.hash);
console.log("Block number:", receipt.blockNumber);
console.log("Gas used:", receipt.gasUsed.toString());
console.log("Logs:", receipt.logs);
});Resources
Building Apps
Development guide
Examples
Code examples
Hardhat Testing
Hardhat test guide
Foundry Testing
Foundry test guide
Next Steps
How is this guide?