skill·2026-03-22·7 min read

Aleo Testing & Debugging

Aleo Testing & Debugging

1. Overview

Leo provides three testing approaches: transition tests (off-chain logic), script tests (on-chain finalize logic), and devnet integration tests (full deployment testing). This skill covers all three plus the interactive debugger.

Version & Canonical Syntax

  • Target: Leo compiler >= 3.5.0
  • Canonical syntax: Programs under test use transition / async transition entry points with separate async function finalize blocks
  • Test file structure: Test files must wrap tests inside a program test_NAME.aleo { } block with @noupgrade async constructor() {}
  • Constructor nuance: leo build --build-tests may compile test files without a constructor, but leo test fails at runtime on current networks when constructors are missing
  • Test syntax target: Use current @test, @should_fail, transition, and script forms shown below
  • External types: When referencing types from another program, use fully qualified names (e.g., my_project.aleo/Token not Token)
  • When docs conflict: Prefer current leo test --help output and this skill's examples

2. Key Concepts

  • @test annotation: Marks a function as a test that runs during leo test
  • @should_fail annotation: Marks a test that is expected to fail (panics, assertions)
  • Transition test: Tests off-chain logic only. Cannot test finalize blocks or mappings.
  • Script test: Tests that can execute finalize blocks, await Futures, and interact with mappings. Uses the script keyword.
  • leo debug: Interactive debugger for stepping through program execution.

3. Test File Organization

Tests live in the tests/ directory:

my_project/
├── src/
│   └── main.leo
├── tests/
│   └── test_my_project.leo    # Test file
└── program.json

4. Transition Tests (Off-Chain Logic)

leo
// tests/test_my_project.leo
import my_project.aleo;

program test_my_project.aleo {
    // Basic passing test
    @test
    transition test_addition() {
        let result: u32 = my_project.aleo/sum(1u32, 2u32);
        assert_eq(result, 3u32);
    }

    // Test with a specific private key
    @test(private_key = "APrivateKey1zkp2kXWazYbnx8tb8xzBiiKkGbvpD6fF94kbTUFEfT9sMH3")
    transition test_with_key() {
        // External record types must be fully qualified
        let token: my_project.aleo/Token = my_project.aleo/mint(
            aleo1axszqqjhuqpzz4duv2slw8j4d6w3fmqczpycqkkvfct57xa8jypqazjg20,
            100u64
        );
        assert_eq(token.amount, 100u64);
    }

    // Expected failure with specific error
    @test
    @should_fail
    transition test_unauthorized() {
        // This should fail because caller != expected admin
        my_project.aleo/admin_only_function();
    }

    // Required for reliable `leo test` execution on current networks
    @noupgrade
    async constructor() {}
}

5. Script Tests (Finalize/Mapping Logic)

Script tests are the ONLY way to test finalize blocks and mapping behavior locally. They use the script keyword instead of transition. Script tests must be inside the same program test_NAME.aleo { } block — a program with only script tests and no transition will fail (every program needs at least one transition).

leo
// tests/test_my_project.leo
import my_project.aleo;

program test_my_project.aleo {
    // Required: at least one transition per test program
    @test
    transition test_basic() {
        assert(true);
    }

    @test
    script test_mapping_update() {
        // Execute the function and capture the Future
        let fut: Future = my_project.aleo/increment(5u64);

        // Execute the finalize block
        fut.await();

        // Verify the mapping was updated correctly
        let value: u64 = Mapping::get(my_project.aleo/counts, aleo1qnr4dkkvkgfqph0vzc3y6z2eu975wnpz2925ntjccd5cfqxtyu8s7pyjh9);
        assert_eq(value, 5u64);
    }

    @test
    script test_async_workflow() {
        // Mint tokens
        let fut: Future = my_project.aleo/mint_public(
            aleo1qnr4dkkvkgfqph0vzc3y6z2eu975wnpz2925ntjccd5cfqxtyu8s7pyjh9,
            1000u64
        );
        fut.await();

        // Check balance
        let balance: u64 = Mapping::get(
            my_project.aleo/account,
            aleo1qnr4dkkvkgfqph0vzc3y6z2eu975wnpz2925ntjccd5cfqxtyu8s7pyjh9
        );
        assert_eq(balance, 1000u64);

        // Transfer
        let fut2: Future = my_project.aleo/transfer_public(
            aleo19y2eyc2cycvdqmycqam60l6uexvfj468xcet65jnnzc5pn8g9ufqg2clp2,
            500u64
        );
        fut2.await();

        // Verify sender balance
        let sender_balance: u64 = Mapping::get(
            my_project.aleo/account,
            aleo1qnr4dkkvkgfqph0vzc3y6z2eu975wnpz2925ntjccd5cfqxtyu8s7pyjh9
        );
        assert_eq(sender_balance, 500u64);
    }

    // Test with randomness
    @test
    script test_with_randomness() {
        let rand_val: field = ChaCha::rand_field();
        // ChaCha::rand_* is available in script tests
        assert_neq(rand_val, 0field);
    }

    // Expected failure in finalize
    @test
    @should_fail
    script test_insufficient_balance() {
        let fut: Future = my_project.aleo/transfer_public(
            aleo19y2eyc2cycvdqmycqam60l6uexvfj468xcet65jnnzc5pn8g9ufqg2clp2,
            9999999u64  // More than available
        );
        fut.await();  // Should fail in finalize
    }

    @noupgrade
    async constructor() {}
}

6. Running Tests

bash
# Run all tests
leo test

# Run with JSON output for CI
leo test --json-output

# Build tests without running (useful for CI compilation checks)
leo build --build-tests

7. Interactive Debugger

The Leo debugger (leo debug) allows stepping through program execution interactively.

Launch the Debugger

bash
leo debug

Debugger Commands

CommandDescription
#intoStep into a function call
#stepStep to the next statement
#overStep over a function call (execute without entering)
#runRun until next breakpoint or end
#break <line>Set a breakpoint at line number
#watch <variable>Watch a variable's value
#printPrint current state
#set_program <name>Switch to a different program's context

Debugger Cheat Codes

leo
// Available in debug mode:
CheatCode::set_block_height(100u32);    // Simulate block height
CheatCode::print_mapping(my_mapping);   // Print entire mapping contents

Debugging Workflow

  1. Add assertions to narrow down the failure
  2. Run leo debug to step through execution
  3. Use #watch to monitor variables at each step
  4. Use #break to stop at specific lines
  5. Use CheatCode::print_mapping() to inspect mapping state

8. Devnet Integration Testing

For full end-to-end testing including deployment and network execution:

bash
# 1. Start devnet
leo devnet --snarkos /path/to/snarkos

# 2. Deploy
leo deploy --broadcast --devnet

# 3. Execute a function
leo execute mint "aleo1..." 100u64 --broadcast --devnet

# 4. Query state
leo query program my_project.aleo --mapping-value account "aleo1..."

# 5. Verify transaction
leo query transaction <tx_id>

9. Common Test Failures and Fixes

FailureCauseFix
"Missing constructor declaration"Test program has no constructorAdd @noupgrade async constructor() {} to the test program block
"A program must have at least one transition"Test file has only script testsAdd at least one @test transition to the test program
Unresolved external typeUsed Token instead of my_project.aleo/TokenUse fully qualified type names for types from other programs
"Mapping operations outside async"Test tries to access mappings in transition testUse script test instead of transition test
Record ownership mismatchTest caller doesn't own the recordUse @test(private_key = "...") to set the correct caller
Assertion failure in finalizeMapping::get on missing keyUse Mapping::get_or_use with a default value
"Future not awaited"Script test captures Future but doesn't .await()Add fut.await() after capturing the Future
Import not foundTest file doesn't import the programAdd import my_project.aleo; at top of test file
Arithmetic overflowChecked arithmetic exceeds type rangeUse larger types or _wrapped variants

10. CI Integration

bash
# CI pipeline commands
leo fmt --check           # Formatting check (fail on bad format)
leo build                  # Compile check
leo build --build-tests    # Compile tests
leo test --json-output     # Run tests with machine-readable output

11. Security Notes

  • Never commit real private keys in @test(private_key = "...") fixtures
  • Keep test-only keys scoped to local/devnet environments and rotate shared CI secrets
  • Treat decrypted record values in tests as sensitive data and avoid logging full plaintext in CI output
  • Use negative tests for authorization boundaries so privileged flows cannot regress silently

12. Performance Notes

  • Start with transition tests for fast feedback and add script tests only where finalize behavior is required
  • Group related finalize assertions into fewer script tests to reduce repeated setup overhead
  • Use leo build --build-tests in CI pre-check stages to catch syntax regressions earlier than full execution
  • Reserve devnet integration tests for end-to-end flows and keep unit logic in local leo test

13. Agent SOP: Testing Workflow

  1. Write tests alongside code: Every function should have at least one test
  2. Use transition tests for pure off-chain logic (record creation, struct manipulation)
  3. Use script tests for any function with async function (finalize) blocks or mapping operations
  4. Include negative tests: Use @should_fail for expected failures (overflow, access control)
  5. Test edge cases: Zero values, maximum values, unauthorized callers
  6. Run leo test after every change: Tests are fast (no proof generation)
  7. Use devnet for integration tests: Deploy + execute + query cycle
  8. If failures occur: map the failure to the matrix above, apply the exact fix, and re-run the specific test before full suite execution
  • Write programs to test using aleo_smart_contracts
  • Deploy tested programs using aleo_deployment
  • Complete test-ready recipes in aleo_cookbook

Sources