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 transitionentry points with separateasync functionfinalize 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-testsmay compile test files without a constructor, butleo testfails at runtime on current networks when constructors are missing - Test syntax target: Use current
@test,@should_fail,transition, andscriptforms shown below - External types: When referencing types from another program, use fully qualified names (e.g.,
my_project.aleo/TokennotToken) - When docs conflict: Prefer current
leo test --helpoutput and this skill's examples
2. Key Concepts
@testannotation: Marks a function as a test that runs duringleo test@should_failannotation: 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
scriptkeyword. 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
| Command | Description |
|---|---|
#into | Step into a function call |
#step | Step to the next statement |
#over | Step over a function call (execute without entering) |
#run | Run until next breakpoint or end |
#break <line> | Set a breakpoint at line number |
#watch <variable> | Watch a variable's value |
#print | Print 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
- Add assertions to narrow down the failure
- Run
leo debugto step through execution - Use
#watchto monitor variables at each step - Use
#breakto stop at specific lines - 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
| Failure | Cause | Fix |
|---|---|---|
| "Missing constructor declaration" | Test program has no constructor | Add @noupgrade async constructor() {} to the test program block |
| "A program must have at least one transition" | Test file has only script tests | Add at least one @test transition to the test program |
| Unresolved external type | Used Token instead of my_project.aleo/Token | Use fully qualified type names for types from other programs |
| "Mapping operations outside async" | Test tries to access mappings in transition test | Use script test instead of transition test |
| Record ownership mismatch | Test caller doesn't own the record | Use @test(private_key = "...") to set the correct caller |
| Assertion failure in finalize | Mapping::get on missing key | Use 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 found | Test file doesn't import the program | Add import my_project.aleo; at top of test file |
| Arithmetic overflow | Checked arithmetic exceeds type range | Use 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-testsin 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
- Write tests alongside code: Every function should have at least one test
- Use transition tests for pure off-chain logic (record creation, struct manipulation)
- Use script tests for any function with
async function(finalize) blocks or mapping operations - Include negative tests: Use
@should_failfor expected failures (overflow, access control) - Test edge cases: Zero values, maximum values, unauthorized callers
- Run
leo testafter every change: Tests are fast (no proof generation) - Use devnet for integration tests: Deploy + execute + query cycle
- 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