Hello, we are currently developing an important tool for businesses.
The system is operational on the local network, but we would like to test it before deploying it to the mainnet, as there is a significant difference in implementation between the local and mainnet versions.
I have noticed that there is no testnet. What solutions are available for testing in an environment that closely resembles the mainnet?
Thank you.
Antoine
Hi Antoine,
You’re right, there’s no public testnet on the IC. In practice there are two complementary approaches; which you need depends on what “difference between local and mainnet” means for your app:
1. PocketIC, which is a deterministic testing framework that runs the IC protocol locally. It’s more faithful than a plain dfx start replica: multiple subnets, time manipulation, deterministic execution, fast integration tests. Great for CI and inter-canister flows. But it’s still local. PocketIC won’t reproduce real cycles economics, threshold ECDSA/Schnorr with the production key_1 (it uses hardcoded test keys), real inter-subnet latency (XNet messages are tick-processed, not network-delayed), or boundary-node behaviour (rate limiting, routing, TLS termination, query caching). Even HTTPS outcalls, which can run live in auto-progress mode, won’t exercise the multi-replica consensus path where transform-function non-determinism actually bites in production.
2. A staging deployment on mainnet: deploy a complete second copy of all your canisters to mainnet, alongside production. Since it runs on the same network, it behaves identically to production. This is what I do, even though I know it might sound a bit funny, I actually feel pretty comfortable with it
I keep two full sets of environment constants (staging + production) and a python script in the repo that promotes one to the other.
-
Canister IDs are hardcoded in many places (Rust source, frontend config, build config), so the switch is a codebase-wide find-and-replace, not a single config edit.
-
It’s not only canister IDs: you also swap admin/controller principals, fee-collector principals, treasury/account identifiers, and any third-party app IDs that differ per environment.
-
Keep the swap in a versioned script with both directions (prod->staging, staging->prod), exclude docs/build artefacts, and always review the diff before committing.
So, PocketIC for deterministic integration tests, and a mainnet staging environment for final pre-production validation that has to behave exactly like the real thing. Surely my approach isn’t that common, and others may be able to give you better advice. But PocketIC is definitely a good tool. And honestly, getting PocketIC running is the easy part. The hard part is writing tests that actually catch the bugs that bite on mainnet. The default failure mode is tests that exercise the happy path and assert the return matches an expected struct. That verifies the function compiles and Candid roundtrips, it doesn’t tell you the canister is safe. But PocketIC really shines when you have thorough, well-designed tests.
@AntekMnm Just a quick follow-up to my previous reply! I realized my manual Python script approach for swapping IDs between staging and production is actually outdated now. Someone more up-to-date with the new icp-cli could probably give you a deeper technical dive, but I wanted to make sure you’re aware of the new native way to handle this.
The new icp-cli tool completely changes the game for staging deployments by decoupling the physical network from your logical setup:
-
Network (The physical layer): Where the code actually runs. This can be local (your PC), a Docker node, or ic (the real mainnet).
-
Environment (The logical layer): A grouping of canisters and settings. You can create custom environments like prod and staging.
Inside the new icp.yaml configuration file, you can explicitly tell icp-cli that both your staging and prod environments should be published to the ic network (the real mainnet).
This natively solves the exact problem I was patching with my custom script:
-
No more hard-coded IDs: The CLI dynamically manages Canister IDs per environment and injects them at runtime (e.g., via ic_env cookies for the frontend). You don’t have to find-and-replace anything.
-
Environment-specific variables: You can define different initialization arguments or settings for your staging environment versus production directly in the YAML file.
-
Build once, deploy many: You compile the .wasm once, deploy it to your mainnet staging environment for live testing, and if it passes, you use that exact same verified hash for the prod deployment.
I highly recommend looking into this rather than my old dfx workaround.
You can find all the details and ask the devs directly in the icp-cli announcements and feedback discussion. PocketIC still remains valid regardless.
Thank you for your replies.
In fact, we have created a system that deploys a canister for the user (they sign it) and they remain the owner, and we install our product in their canister.
I have built a working local version, but in reality the automated deployment system (I’ve done this for mainnet as well) differs between local and production.
I would like to test the production version before deploying it to mainnet. To check whether the signature and the system deploy correctly. Locally, we do not use ICP ID.