Learn how to write and run tests for your Leo programs using the built-in test framework.
Test Framework Overview
Leo includes a comprehensive test framework located in crates/test-framework/ that supports:
Unit tests within test programs
Integration tests in the tests/ directory
Expectation-based testing with automatic output comparison
Parallel test execution
Writing Unit Tests
Test Program Structure
Tests are written in separate .leo files in the tests/ directory:
my_project/
├── src/
│ └── main.leo
└── tests/
└── test_my_project.leo
Basic Test Example
Create a test program that imports your main program:
tests/test_my_project.leo
import my_project.aleo;
program test_my_project.aleo {
@test
fn test_addition() {
let result: u32 = my_project.aleo/main(2u32, 3u32);
assert_eq(result, 5u32);
}
@noupgrade
constructor() {}
}
Test programs must import the program they’re testing and use the @test annotation on test functions.
Test Annotations
Leo provides several test annotations:
Marks a function as a test case: @test
fn test_basic_math() {
let result: u32 = 2u32 + 2u32;
assert_eq(result, 4u32);
}
Indicates the test should fail or panic: @test
@should_fail
fn test_invalid_input() {
let result: u32 = my_project.aleo/main(2u32, 3u32);
assert_eq(result, 3u32); // This assertion fails
}
Script Tests
You can also write standalone test scripts:
import my_project.aleo;
@test
script test_it() {
let result: u32 = my_project.aleo/main(1u32, 2u32);
assert_eq(result, 3u32);
}
Scripts are lightweight test cases that don’t require a full program definition.
Assertions
assert_eq
Compare two values for equality:
@test
fn test_equality() {
let a: u32 = 10u32;
let b: u32 = 10u32;
assert_eq(a, b);
}
assert_neq
Compare two values for inequality:
@test
fn test_inequality() {
let a: u32 = 10u32;
let b: u32 = 20u32;
assert_neq(a, b);
}
Complex Assertions
Test with complex data structures:
struct Token {
owner: address,
amount: u64,
}
@test
fn test_struct_creation() {
let token: Token = Token {
owner: aleo1xxx...xxx,
amount: 100u64,
};
assert_eq(token.amount, 100u64);
}
Running Tests
Run All Tests
Execute all tests in your project:
Run Specific Tests
Use the TEST_FILTER environment variable to run specific tests:
TEST_FILTER = addition leo test
This runs only tests whose names contain “addition”.
Update Expectations
When test output changes intentionally, update expectation files:
Only use UPDATE_EXPECT=1 when you’ve verified the new output is correct. This overwrites all expectation files.
Test Framework Internals
The Leo test framework (from crates/test-framework/src/lib.rs):
Test Discovery
Scans the tests/{category} directory for .leo files: let paths : Vec < PathBuf > = WalkDir :: new ( & tests_dir )
. into_iter ()
. flatten ()
. filter_map ( | entry | {
let path = entry . path ();
if path_str . ends_with ( ".leo" ) {
Some ( path . into ())
} else {
None
}
})
. collect ();
Test Execution
Runs each test through the runner function: let result_output = std :: panic :: catch_unwind ( || runner ( & contents ));
Output Comparison
Compares output against expectation files in expectations/{category}/: let expected = fs :: read_to_string ( & expectation_path ) ? ;
if output != expected {
// Test failed - show diff
print_diff ( & expected , & output );
}
Parallel Execution
Tests run in parallel by default using Rayon:
#[cfg(not(feature = "no_parallel" ))]
let results : Vec < TestResult > = paths . par_iter () . map ( run_test ) . collect ();
For sequential execution, build with --features no_parallel.
Integration Testing
Testing with Dependencies
Test programs can have their own dependencies:
{
"program" : "my_program.aleo" ,
"version" : "0.1.0" ,
"dependencies" : null ,
"dev_dependencies" : [
{
"name" : "test_utils.aleo" ,
"location" : "local" ,
"path" : "../test_utils"
}
]
}
Testing with Multiple Programs
Test interactions between programs:
tests/test_interaction.leo
import program_a.aleo;
import program_b.aleo;
program test_interaction.aleo {
@test
fn test_cross_program_call() {
let result_a: u32 = program_a.aleo/compute(5u32);
let result_b: u32 = program_b.aleo/process(result_a);
assert_eq(result_b, 10u32);
}
@noupgrade
constructor() {}
}
Testing Async and Finalize
Testing Mappings
Test programs that use on-chain storage:
program registry.aleo {
mapping users: address => bool;
fn register() -> Final {
let caller = self.caller;
return final { finalize_register(caller); };
}
}
final fn finalize_register(addr: address) {
Mapping::set(users, addr, true);
}
Test the finalize logic:
import registry.aleo;
program test_registry.aleo {
@test
fn test_registration() {
let result: Final = registry.aleo/register();
// Test the Final return value
}
@noupgrade
constructor() {}
}
Testing Records
Test private record creation and manipulation:
@test
fn test_record_creation() {
let token: Token = create_token();
assert_eq(token.amount, 100u64);
assert_eq(token.owner, self.signer);
}
Compiler Test Categories
The Leo compiler includes extensive tests organized by category:
tests/tests/
├── compiler/
│ ├── address/ # Address type tests
│ ├── array/ # Array tests
│ ├── function/ # Function tests
│ ├── mappings/ # Mapping tests
│ ├── records/ # Record tests
│ └── structs/ # Struct tests
├── parser/ # Parser tests
└── passes/ # Compiler pass tests
Running Compiler Tests
# Run all compiler tests
cargo test -p leo-compiler
# Run specific category
TEST_FILTER = function cargo test -p leo-compiler
# Update parser expectations
UPDATE_EXPECT = 1 cargo test -p leo-parser
Best Practices
Test One Thing
Each test should verify a single behavior: @test
fn test_addition() {
let result: u32 = add(2u32, 3u32);
assert_eq(result, 5u32);
}
@test
fn test_multiplication() {
let result: u32 = multiply(2u32, 3u32);
assert_eq(result, 6u32);
}
Use Descriptive Names
Test names should describe what they verify: @test
fn test_transfer_reduces_sender_balance() { ... } // Good
@test
fn test1() { ... } // Avoid
Test Edge Cases
Include tests for boundary conditions: @test
fn test_zero_input() {
let result: u32 = process(0u32);
assert_eq(result, 0u32);
}
@test
fn test_max_value() {
let max: u32 = 4294967295u32;
let result: u32 = process(max);
assert_eq(result, max);
}
Test Failure Cases
Verify error conditions with @should_fail: @test
@should_fail
fn test_division_by_zero() {
let result: u32 = divide(10u32, 0u32);
}
CI/CD Integration
Leo tests integrate with continuous integration:
CircleCI Example
version : 2.1
jobs :
test :
docker :
- image : cimg/rust:1.70
steps :
- checkout
- run : cargo test --all-features
- run : cargo test -p leo-compiler
GitHub Actions Example
name : Test
on : [ push , pull_request ]
jobs :
test :
runs-on : ubuntu-latest
steps :
- uses : actions/checkout@v2
- uses : actions-rs/toolchain@v1
- run : cargo test --workspace
Next Steps