⚠️Blast API (blastapi.io) ends Oct 31. Migrate to Dwellir and skip Alchemy's expensive compute units.
Switch Today →
Skip to main content

Testing

Testing is crucial for building reliable smart contracts on Aptos. Move provides a comprehensive testing framework that supports unit tests, integration tests, and end-to-end testing scenarios. Proper testing helps catch bugs early, validates business logic, and provides confidence when deploying contracts that manage valuable assets.

Overview#

Move's testing framework allows you to write tests directly in your Move source files using the #[test] attribute. Tests can interact with the blockchain state, simulate different signers, and validate both success and failure scenarios. The Aptos CLI provides commands to run tests with detailed output and coverage reporting.

Unit Testing Basics#

Unit tests verify individual functions and modules in isolation:

module 0x1::calculator {
public fun add(a: u64, b: u64): u64 {
a + b
}

public fun divide(a: u64, b: u64): u64 {
assert!(b != 0, 1); // EDIVIDE_BY_ZERO
a / b
}

#[test]
fun test_add() {
assert!(add(2, 3) == 5, 0);
assert!(add(0, 0) == 0, 0);
assert!(add(100, 200) == 300, 0);
}

#[test]
fun test_divide() {
assert!(divide(10, 2) == 5, 0);
assert!(divide(100, 10) == 10, 0);
}

#[test]
#[expected_failure(abort_code = 1)]
fun test_divide_by_zero() {
divide(10, 0); // Should abort with error code 1
}
}

Testing with Signers#

Test functions can receive signer parameters for testing account-specific logic:

module 0x1::vault {
use std::signer;

struct Vault has key {
balance: u64
}

public fun create_vault(account: &signer, initial: u64) {
move_to(account, Vault { balance: initial });
}

public fun deposit(account: &signer, amount: u64) acquires Vault {
let vault = borrow_global_mut<Vault>(signer::address_of(account));
vault.balance = vault.balance + amount;
}

#[test(account = @0x1)]
fun test_create_vault(account: &signer) {
create_vault(account, 100);
assert!(exists<Vault>(@0x1), 0);
}

#[test(account = @0x1)]
fun test_deposit(account: &signer) acquires Vault {
create_vault(account, 100);
deposit(account, 50);
let vault = borrow_global<Vault>(@0x1);
assert!(vault.balance == 150, 0);
}

#[test(account = @0x1)]
#[expected_failure]
fun test_double_create_fails(account: &signer) {
create_vault(account, 100);
create_vault(account, 200); // Should fail - resource already exists
}
}

Multi-Account Testing#

Test scenarios involving multiple accounts:

#[test(from = @0x1, to = @0x2)]
fun test_transfer(from: &signer, to: &signer) acquires TokenStore {
// Setup
initialize(from, 1000);
initialize(to, 0);

// Execute transfer
transfer(from, signer::address_of(to), 100);

// Verify balances
assert!(balance_of(@0x1) == 900, 0);
assert!(balance_of(@0x2) == 100, 0);
}

#[test(alice = @0x1, bob = @0x2, charlie = @0x3)]
fun test_multi_party_transaction(
alice: &signer,
bob: &signer,
charlie: &signer
) acquires TokenStore {
// Complex multi-account test scenario
}

Testing Events#

Validate that events are emitted correctly:

#[test(account = @0x1)]
fun test_transfer_event(account: &signer) acquires TokenStore, EventStore {
use aptos_framework::event;

initialize(account, 1000);

// Get initial event counter
let event_store = borrow_global<EventStore>(@0x1);
let initial_count = event::counter(&event_store.transfer_events);

// Execute operation
transfer(account, @0x2, 100);

// Verify event was emitted
let final_count = event::counter(&event_store.transfer_events);
assert!(final_count == initial_count + 1, 0);
}

Test Helpers and Setup#

Create helper functions for common test scenarios:

#[test_only]
module 0x1::test_helpers {
use std::signer;
use 0x1::vault;

public fun setup_vault(account: &signer, balance: u64) {
vault::create_vault(account, balance);
}

public fun create_test_accounts(): (signer, signer, signer) {
// Test-only function to create multiple accounts
}
}

// Use in tests
#[test(account = @0x1)]
fun test_with_helper(account: &signer) {
test_helpers::setup_vault(account, 100);
// Continue test
}

Real-World Use Cases#

  1. Token Contract Validation: Test minting, burning, transfers, and balance tracking to ensure token economics work correctly under all conditions.

  2. Access Control Testing: Verify that privileged functions can only be called by authorized accounts and that unauthorized access attempts fail properly.

  3. Edge Case Coverage: Test boundary conditions like zero amounts, maximum values, empty collections, and invalid inputs.

  4. Failure Scenario Testing: Use #[expected_failure] to verify that functions abort correctly when given invalid inputs or when preconditions aren't met.

  5. Upgrade Compatibility: Test that upgraded modules maintain backward compatibility with existing on-chain state and don't break existing functionality.

  6. Integration Testing: Test interactions between multiple modules to verify that complex workflows operate correctly end-to-end.

Best Practices#

Test Both Success and Failure: Write tests for both happy paths and error conditions. Use #[expected_failure] to verify error handling.

Use Descriptive Test Names: Name tests clearly to indicate what they're testing (e.g., test_transfer_insufficient_balance_fails).

Keep Tests Independent: Each test should set up its own state and not depend on execution order or state from other tests.

Test Edge Cases: Include tests for boundary values, empty inputs, maximum values, and unusual but valid scenarios.

Use Test-Only Code: Mark helper functions and modules with #[test_only] to prevent them from being included in production builds.

Deterministic Testing: Avoid randomness or time-dependent logic in tests to ensure reproducibility.

Coverage Goals: Aim for comprehensive coverage of all public functions and critical internal logic paths.

Running Tests#

# Run all tests in package
aptos move test --package-dir .

# Run tests with coverage
aptos move test --package-dir . --coverage

# Run specific test
aptos move test --package-dir . --filter test_transfer

# Run tests with gas profiling
aptos move test --package-dir . --gas

# Verbose output
aptos move test --package-dir . --verbose

Advanced Testing Patterns#

// Property-based testing pattern
#[test]
fun test_add_commutative() {
let values = vector[1, 5, 10, 50, 100, 1000];
let i = 0;
while (i < vector::length(&values)) {
let j = 0;
while (j < vector::length(&values)) {
let a = *vector::borrow(&values, i);
let b = *vector::borrow(&values, j);
assert!(add(a, b) == add(b, a), 0);
j = j + 1;
};
i = i + 1;
}
}

// State machine testing
#[test(account = @0x1)]
fun test_state_transitions(account: &signer) acquires StateMachine {
initialize(account);
assert!(get_state(@0x1) == STATE_INITIAL, 0);

transition_to_active(account);
assert!(get_state(@0x1) == STATE_ACTIVE, 0);

transition_to_complete(account);
assert!(get_state(@0x1) == STATE_COMPLETE, 0);
}