Testing Contracts Locally
Local testing gives you fast iteration and deterministic reproducibility. But local EVM emulators cannot fully model OXN's confidentiality behavior. This page explains what works locally, what doesn't, and how to structure your test pyramid.
What local networks can test
Standard Solidity contract logic — everything that would run on Ethereum unchanged — tests fine locally:
- Contract state transitions
- Access control (using
msg.sender) - Event emission (topic and data structure)
- Reverts and error conditions
- Interactions between multiple contracts
- Gas usage patterns (approximately)
What local networks CANNOT test
The following behaviors depend on OXN's runtime, not the EVM alone:
- Encrypted calldata handling — a local network sees calldata in plain, so any assertion about "did this call arrive encrypted?" is meaningless.
- Confidential storage — local storage is public; a test that checks "an outsider cannot read this slot" gives a false positive against every local network.
- Signed queries — the signed-query path requires the OXN gateway. Locally,
eth_callis unauthenticated by default. - Confidential precompiles —
Sapphire.randomBytes,Sapphire.encrypt, etc. do not exist on standard Hardhat network. - PUSH0 rejection — local networks (Hardhat, Anvil) implement PUSH0 by default. A contract compiled with
shanghaiwill run fine locally and only fail on real OXN.
Recommended test pyramid
| Layer | Location | Coverage |
|---|---|---|
| Unit tests | Hardhat / Foundry local | Business logic, math, access control, revert conditions. |
| Integration tests | OXN Testnet | Confidentiality behavior, encrypted calldata, signed queries. |
| End-to-end tests | OXN Testnet + real wallet | Wallet interaction, faucet flow, user-facing paths. |
Every dApp should run at least some integration tests against real OXN. Don't assume Hardhat green means production green.
Hardhat local testing
Standard Hardhat testing works out of the box:
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("MyToken", function () {
it("mints initial supply to deployer", async function () {
const [deployer] = await ethers.getSigners();
const Token = await ethers.getContractFactory("MyToken");
const token = await Token.deploy(ethers.parseUnits("1000", 18));
expect(await token.balanceOf(deployer.address))
.to.equal(ethers.parseUnits("1000", 18));
});
});
Run with npx hardhat test. Hardhat spins up an in-memory network, deploys, and asserts.
Foundry local testing
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "forge-std/Test.sol";
import "../src/MyToken.sol";
contract MyTokenTest is Test {
MyToken token;
address deployer = address(this);
function setUp() public {
token = new MyToken(1_000_000 ether);
}
function testInitialSupply() public {
assertEq(token.balanceOf(deployer), 1_000_000 ether);
}
}
Run with forge test. Foundry's built-in Test contract provides assertion helpers and cheatcodes.
Fixtures and snapshots
For faster tests, use fixtures to avoid redeploying:
const { loadFixture } = require("@nomicfoundation/hardhat-network-helpers");
async function deployTokenFixture() {
const [deployer, other] = await ethers.getSigners();
const Token = await ethers.getContractFactory("MyToken");
const token = await Token.deploy(ethers.parseUnits("1000", 18));
return { token, deployer, other };
}
describe("MyToken transfers", function () {
it("transfers tokens", async function () {
const { token, deployer, other } = await loadFixture(deployTokenFixture);
await token.transfer(other.address, ethers.parseUnits("100", 18));
expect(await token.balanceOf(other.address))
.to.equal(ethers.parseUnits("100", 18));
});
});
Fixtures snapshot the network state after setUp and restore it before each test, making a full test suite 10-100x faster than redeploying.
Time and block manipulation
Hardhat and Foundry both expose "cheatcodes" to move time forward or set specific block conditions in local tests. Use these for time-locked contracts:
const { time } = require("@nomicfoundation/hardhat-network-helpers");
// Fast-forward 1 hour
await time.increase(3600);
// Jump to a specific timestamp
await time.increaseTo(1700000000);
vm.warp(block.timestamp + 3600); // fast-forward 1 hour
vm.roll(block.number + 100); // advance 100 blocks
These cheatcodes are local-only. Real OXN does not accept time manipulation from clients.
Next steps
- Testing Confidential Behavior — integration testing against real OXN
- Deploying Contracts — moving from tests to a live network