Expert on testing Move smart contracts on Aptos, including unit tests, integration tests, Move Prover formal verification, debugging strategies, and test coverage. Triggers on keywords move test, unit test, integration test, move prover, formal verification, debug, coverage, assert, expect
Provide expert guidance on testing Move smart contracts on Aptos blockchain, including unit tests, integration tests, formal verification with Move Prover, debugging strategies, and achieving comprehensive test coverage.
Auto-invoke when users mention:
aptos move test, aptos move proveassert!, test expectations, error codes#[test]
fun test_basic_functionality() {
// Test code here
assert!(condition, ERROR_CODE);
}
#[test(account = @0x1)]
fun test_with_signer(account: &signer) {
// Test with signer parameter
}
#[test]
#[expected_failure(abort_code = ERROR_CODE)]
fun test_expected_failure() {
// Code that should fail
}
#[test] - Basic test function#[test(param = @address)] - Test with named parameters (signers/addresses)#[test_only] - Functions/modules only compiled in test mode#[expected_failure] - Test should abort#[expected_failure(abort_code = N)] - Test should abort with specific codeWhen a user asks about Move testing:
Common scenarios:
- Writing unit tests for new functions
- Testing resource operations (move_to, move_from, borrow_global)
- Verifying error conditions and abort codes
- Integration testing with multiple modules
- Formal verification with Move Prover
- Debugging test failures
- Improving test coverage
Unit Tests:
#[test] attributeIntegration Tests:
Formal Verification:
Structure your response:
#[test_only]
use std::signer;
#[test(account = @0x123)]
fun test_resource_creation(account: &signer) {
let addr = signer::address_of(account);
// Create resource
create_resource(account);
// Verify resource exists
assert!(exists<MyResource>(addr), ERROR_RESOURCE_NOT_FOUND);
// Verify resource state
let resource = borrow_global<MyResource>(addr);
assert!(resource.value == expected_value, ERROR_INVALID_STATE);
}
#[test]
#[expected_failure(abort_code = ERROR_INSUFFICIENT_BALANCE)]
fun test_insufficient_balance() {
// Setup account with low balance
// Attempt operation that should fail
transfer(from, to, amount_too_large);
}
#[test(alice = @0x123, bob = @0x456)]
fun test_transfer(alice: &signer, bob: &signer) {
let alice_addr = signer::address_of(alice);
let bob_addr = signer::address_of(bob);
// Setup initial state
initialize(alice);
initialize(bob);
// Perform transfer
transfer(alice, bob_addr, 100);
// Verify balances
assert!(get_balance(alice_addr) == 900, ERROR_INVALID_BALANCE);
assert!(get_balance(bob_addr) == 100, ERROR_INVALID_BALANCE);
}
#[test_only]
module test_helpers {
use std::signer;
public fun setup_account(account: &signer): address {
let addr = signer::address_of(account);
// Common setup logic
addr
}
public fun create_test_token(account: &signer, amount: u64) {
// Create tokens for testing
}
}
spec module {
// Module-level invariants
invariant forall addr: address:
exists<Balance>(addr) ==> global<Balance>(addr).value >= 0;
}
spec transfer {
// Pre-conditions
requires sender_balance >= amount;
requires sender != recipient;
// Post-conditions
ensures global<Balance>(sender).value == old(global<Balance>(sender).value) - amount;
ensures global<Balance>(recipient).value == old(global<Balance>(recipient).value) + amount;
// Abort conditions
aborts_if sender_balance < amount;
}
# Verify all specs in package
aptos move prove
# Verify specific module
aptos move prove --filter MyModule
# Verbose output
aptos move prove --verbose
# Run all tests in package
aptos move test
# Run specific test
aptos move test --filter test_name
# Run tests with coverage
aptos move test --coverage
# Run with gas profiling
aptos move test --gas
# Verbose output
aptos move test --verbose
Running Move unit tests
[ PASS ] 0x1::my_module::test_success
[ FAIL ] 0x1::my_module::test_failure
[ TIMEOUT ] 0x1::my_module::test_slow
Error: Assertion failed
┌── my_module.move:42:9 ───
│
42 │ assert!(balance == 100, ERROR_INVALID_BALANCE);
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
#[test_only]
use std::debug;
#[test]
fun test_with_debug() {
debug::print(&b"Starting test");
debug::print(&value);
// Test logic
}
// Instead of one large test
#[test]
fun test_complex_workflow() {
setup();
step1();
step2();
step3();
verify();
}
// Break into multiple tests
#[test]
fun test_step1() { /* ... */ }
#[test]
fun test_step2() { /* ... */ }
#[test]
fun test_step3() { /* ... */ }
const ERROR_INVALID_AMOUNT: u64 = 1;
const ERROR_INSUFFICIENT_BALANCE: u64 = 2;
const ERROR_UNAUTHORIZED: u64 = 3;
// In code
assert!(amount > 0, ERROR_INVALID_AMOUNT);
// In test
#[expected_failure(abort_code = ERROR_INVALID_AMOUNT)]
fun test_zero_amount() { /* ... */ }
✅ Critical Paths:
✅ Error Conditions:
✅ Edge Cases:
✅ Invariants:
Aim for:
- 100% of public functions
- 100% of abort conditions
- All state transitions
- All access control checks
#[test_only]
fun setup_test_env(account: &signer): TestContext {
initialize_module(account);
create_test_resources(account);
TestContext { /* ... */ }
}
#[test(account = @0x1)]
fun test_feature(account: &signer) {
let ctx = setup_test_env(account);
// Use ctx
// No teardown needed (Move tests are isolated)
}
#[test_only]
fun verify_transfer_amount(amount: u64, expected_result: bool) {
if (expected_result) {
transfer(sender, receiver, amount);
} else {
// Should fail
}
}
#[test]
fun test_valid_amounts() {
verify_transfer_amount(1, true);
verify_transfer_amount(100, true);
verify_transfer_amount(1000, true);
}
#[test]
#[expected_failure]
fun test_invalid_amounts() {
verify_transfer_amount(0, false);
}
#[test]
fun test_state_consistency(account: &signer) {
let addr = signer::address_of(account);
// Initial state
let before = get_state(addr);
// Perform operation
perform_operation(account);
// Verify state changes
let after = get_state(addr);
assert!(after.field1 == before.field1 + delta, ERROR_STATE);
}