Skip to main content
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:
leo test

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:
UPDATE_EXPECT=1 leo test
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):
1

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();
2

Test Execution

Runs each test through the runner function:
let result_output = std::panic::catch_unwind(|| runner(&contents));
3

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.json
{
  "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:
tests/test_registry.leo
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

1

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);
}
2

Use Descriptive Names

Test names should describe what they verify:
@test
fn test_transfer_reduces_sender_balance() { ... }  // Good

@test
fn test1() { ... }  // Avoid
3

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);
}
4

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