| name | sui-tester |
| description | Use when writing Move tests, setting up test suites, running gas benchmarks, or planning test strategy for SUI contracts. Triggers on "write tests", "test this module", "#[test]", "test coverage", "gas benchmark", "property-based test", or any Move testing task. Use even for simple "how do I test this function" questions. |
SUI Tester
Complete testing solution for SUI Move contracts and frontend applications.
Overview
This skill provides comprehensive testing across all layers:
- Unit Tests - Test individual Move functions
- Integration Tests - Test module interactions
- E2E Tests - Test complete user journeys (frontend + contract)
- Property-Based Tests - Test invariants with random inputs
- Gas Benchmarks - Measure and track gas consumption
SUI Protocol 127 Testing Updates (shipped testnet v1.74.0; now v1.74.1 / P128 on both testnet and mainnet)
Key changes affecting testing (June 2026):
- Move Linter (P128 / v1.74.1+):
sui move lint runs Move linters on the package. Lints also run in sui move build/test by default — --no-lint to skip, --lint for extra linters. Wire into CI alongside sui move test.
- gRPC
SimulateTransaction gasless tier (P127): now accepts gas_price=0 for gasless-tier-eligible transactions instead of erroring — useful for sponsored-tx dry runs.
- Regex Test Filtering: Test filtering uses regex. Use
sui move test --filter "regex_pattern" for precise test selection.
- poseidon_bn254: Available on all networks. Add tests for ZK-related functions using
sui::poseidon::poseidon_bn254.
- TxContext Flexible Positioning:
TxContext can be in any argument position. Update integration tests if they assume last-position TxContext.
- gRPC Data Access: Integration tests must use gRPC client — JSON-RPC Quorum Driver is disabled, permanent deactivation 2026-07-31.
#[error] Annotation: Use #[error] on error constants for human-readable abort messages. Update #[expected_failure] tests to reference constant names, not hardcoded values.
- GraphQL Simulation:
events field removed from simulateResult. Access events via effects.events() in dry-run tests.
- Gas Re-benchmarking: The New Move VM on testnet may produce different gas profiles compared to Protocol 118. If your tests assert specific gas values, re-run
sui move test --gas-limit and update expected values.
- Decoded Object Inspection:
sui client object now shows decoded struct fields — useful for manual verification during integration tests.
Sender impersonation on a forked network (sui-fork + --skip-signing)
The sui-fork tool spins up a local network forked from mainnet/testnet/devnet state, then you submit transactions unsigned under any sender — useful for reproducing user-reported bugs without their keys. sui-fork is a separate crate in MystenLabs/sui (crates/sui-fork, subcommands start / status / advance-clock / advance-checkpoint); it's not shipped via suiup, build it with cargo build -p sui-fork.
sui-fork start --network testnet
sui client new-env --alias local-fork --rpc http://127.0.0.1:9000
sui client switch --env local-fork
sui client call --package <pkg> --module <mod> --function <fn> \
--sender 0x<address> \
--skip-signing
--skip-signing (a sui client tx flag, not a sui replay flag): "Submit the transaction without signatures for forked networks that support sender impersonation. Only intended for local forked-network testing." Use this when an integration test needs to mimic real on-chain state under a specific signer.
Quick Start
sui move test
sui move test --coverage
sui move coverage summary
sui move test --filter "test_create_listing"
sui move test --filter "test_.*listing"
Test Types
Aim for a test pyramid — most coverage cheap and fast at the bottom, a few expensive checks at the top:
E2E Tests (5%)
/ \
Integration (15%)
/ \
Unit Tests (80%)
1. Move Unit Tests
#[test]
fun test_create_listing() {
let seller = @0xA;
let mut scenario = test_scenario::begin(seller);
// Reusable helpers keep setup DRY across tests
let nft = create_test_nft(&mut scenario);
let listing = create_listing(nft, 1000, scenario.ctx());
assert!(price(&listing) == 1000, 0);
assert!(seller(&listing) == seller, 1);
test_scenario::end(scenario);
}
2. Integration Tests
Test cross-module interactions (marketplace + royalty).
3. Frontend E2E Tests
test('complete buy flow', async ({ page }) => {
await page.goto('http://localhost:5173');
await page.click('button:has-text("Connect Wallet")');
await page.click('button:has-text("Buy Now")');
await expect(page.locator('text=Purchase successful')).toBeVisible();
});
4. Property-Based Tests
#[test]
fun property_price_distribution() {
// Test invariant: total = seller + royalty + fee
let iterations = 100;
// ... verify invariant holds
}
5. Gas Benchmarks
sui move test --gas-profile
Test Coverage
Target: >90% code coverage for core modules
Quick Coverage Check
sui move test --coverage
sui move coverage summary
Automated Coverage Analysis Tools
This skill includes Python scripts in scripts/ for detailed coverage analysis:
SCRIPTS=<plugin_path>/skills/sui-tester/scripts
cd /path/to/move/package
sui move test --coverage --trace
python3 $SCRIPTS/analyze_source.py -m <module_name>
python3 $SCRIPTS/analyze_source.py -m <module_name> -o coverage.md
python3 $SCRIPTS/analyze_source.py -m <module_name> --json
sui move coverage lcov
python3 $SCRIPTS/analyze_lcov.py lcov.info -s sources/ --issues-only
sui move coverage bytecode --module <name> | python3 $SCRIPTS/parse_bytecode.py
script -q /dev/null sui move coverage source --module <name> | python3 $SCRIPTS/parse_source.py
Coverage Improvement Workflow
- Analyze — Run
analyze_source.py to get uncovered segments
- Identify gaps:
- Uncalled functions → write tests that call them
- Uncovered assertions → write
#[expected_failure] tests
- Untaken branches → write tests for both if/else paths
- Write tests for each gap (see patterns below)
- Verify — Re-run analysis to confirm improvement
- Repeat until >90% coverage
Coverage Test Patterns
A. Uncalled function:
#[test]
fun test_<function_name>() {
let mut ctx = tx_context::dummy();
<function_name>(&mut ctx);
// Assert expected behavior
}
B. Assertion failure path:
#[test]
#[expected_failure(abort_code = <ERROR_CONST>)]
fun test_<function>_fails_when_<condition>() {
let mut ctx = tx_context::dummy();
// Setup state that triggers the assertion failure
<function_call_that_should_fail>();
}
C. Branch coverage (if/else):
#[test]
fun test_<function>_when_true() { /* condition = true path */ }
#[test]
fun test_<function>_when_false() { /* condition = false path */ }
Common Mistakes
❌ Not using test_scenario properly
- Problem: Tests fail with "object not found" errors
- Fix: Always call
test_scenario::next_tx between transactions, clean up with test_scenario::end
❌ Testing with unrealistic gas budgets
- Problem: Tests pass but fail in production due to gas limits
- Fix: Set realistic gas budgets in tests, use
--gas-limit flag
❌ Ignoring test cleanup
- Problem: Objects leak between tests, intermittent failures
- Fix: Delete all created objects or use
#[expected_failure] for abort tests
❌ Not testing error cases
- Problem: Production failures from unexpected inputs
- Fix: Test all
assert! and abort paths with #[expected_failure(abort_code = X)]
❌ Skipping property-based tests for math
- Problem: Edge cases cause overflow/underflow in production
- Fix: Test invariants with 100+ random inputs (prices, quantities, percentages)
❌ Not benchmarking gas costs
- Problem: Expensive operations drain user funds
- Fix: Run
sui move test --gas-profile, track gas per operation
❌ E2E tests without proper wallet setup
- Problem: Tests fail on wallet connection
- Fix: Use Playwright with wallet mock or testnet faucet automation
Configuration
Test execution targets:
- Unit tests: <30 seconds
- Integration tests: <2 minutes
- E2E tests: <10 minutes
- Full suite: <15 minutes
For installing the CLI, localnet setup, or faucet tokens, see the sui-install skill.