Skip to main content
Version: 4.4.0

Testing, Testing, 123

After deployment, an application stays on the ledger permanently. Thus, consider each edge case and test your code fully. You can use the following tools and techniques.

Choosing a Testing Strategy

ToolBest forNotes
leo testLogic, record fields, mappingsFast. No network round-trip. No fee credits required
leo devnodeEnd-to-end deploy/execute cycles, multi-program interactionNo snarkOS required. Proof generation can be skipped
leo devnetFull consensus scenarios, multi-validator behaviorRequires a snarkOS installation. Heavier setup
TestnetFinal validation before mainnetReal credits required. Use the Aleo faucet

Start with leo test for pure logic. Use leo devnode to test deployment and execution cycles against a live node. Use leo devnet when the scenario requires full consensus behavior. Use Testnet for final validation before Mainnet.

Unit and Integration Testing

The Leo testing framework enables developers to validate their Leo program logic by writing unit and integration tests. Tests are written in Leo and are located in a tests/ subdirectory of the main Leo project directory.

example_program
├── build
│ ├── imports
│ │ └── test_example_program.aleo
│ ├── main.aleo
│ └── program.json
├── outputs
├── src
│ └── main.leo
├── tests
│ └── test_example_program.leo
└── program.json

The test file is a Leo program that imports the program in main.leo. The test functions will all be annotated with @test above the function declaration.

A test program can name a library or program in the package dependencies. Thus, tests can construct dependency types or call dependency functions directly. See dependencies vs. dev_dependencies for visibility rules.

This tutorial will use an example program which can be found in the example's repository.

info

You can add multiple .leo files to the test directory. Each test file name must match the program name in that file. For example, test_example_program.leo must contain the program name test_example_program.aleo.

Leo compiles each test file separately. Each test file is a separate test program. Test files are not modules in one program.

Testing Entry Functions

The example_program.leo program contains an entry function which returns the sum of two u32 inputs.

fn simple_addition(public a: u32, b: u32) -> u32 {
let c: u32 = a + b;
return c;
}

test_example_program.leo contains two tests. They verify the correct sum and the failure that occurs when the output does not match the input sum.

@test
fn test_simple_addition() {
let result: u32 = example_program.aleo::simple_addition(2u32, 3u32);
assert_eq(result, 5u32);
}

The @should_fail annotation should be added after the @test annotation for tests that are expected to fail.

@test
@should_fail
fn test_simple_addition_fail() {
let result: u32 = example_program.aleo::simple_addition(2u32, 3u32);
assert_eq(result, 3u32);
}

Testing as a Specific Account

By default, every @test function runs as the same fixed test account. The corresponding address is what std::ctx::caller() and std::ctx::signer() resolve to inside the test. The default key is:

APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH

To run a single test as a different account, pass a private_key argument to the annotation:

// Run this test as a specific account, not the default test account.
// `std::ctx::caller()` and `std::ctx::signer()` resolve to the address derived from
// the supplied private key for the duration of this test.
@test(private_key = "APrivateKey1zkpG9Af9z5Ha4ejVyMCqVFXRKknSm8L1ELEwcc4htk9YhVK")
fn test_as_specific_account() {
let result: u32 = example_program.aleo::simple_addition(2u32, 3u32);
assert_eq(result, 5u32);
}

To test an access-controlled entry point, pair a privileged test with a @should_fail test. Run the second test with the default or another nonprivileged account. The privileged test uses @test(private_key = "...") to override the caller. The failing test uses @test, so it uses the default test account:

@test(private_key = "APrivateKey1zkpG9Af9z5Ha4ejVyMCqVFXRKknSm8L1ELEwcc4htk9YhVK")
fn test_admin_can_pause() {
example_program.aleo::pause();
}

@test
@should_fail
fn test_non_admin_cannot_pause() {
example_program.aleo::pause();
}

private_key is the only recognized argument to @test. Passing any other key (for example @test(seed = ...)) is a compile error. The value must be a string literal containing a valid Leo private key.

Testing Leo Types

Developers can test that record and struct fields match their expected values. In example_program.leo, a record is minted by an entry function shown here:

fn mint_record(x: field) -> Example {
return Example {
owner: std::ctx::signer(),
x,
};
}

The corresponding test in test_example_program.leo checks that the Record field contains the correct value:

@test
fn test_record_maker() {
let r: example_program.aleo::Example = example_program.aleo::mint_record(0field);
assert_eq(r.x, 0field);
}
info

Each test file is required to have at least one @test fn function.

Modeling Onchain State

The Leo test framework executes tests in the real VM. Thus, @test fn functions fully support on-chain mappings and storage without special syntax. Call entry functions that return Final in the same way as other functions. The test run executes the finalization.

For end-to-end and integration testing against a live network or a local devnet, use the SDK directly or snarkVM as a library.

Testing Library Packages

leo test works on library packages directly — no wrapper program is needed. Place test files in the tests/ directory of the library project and call library functions using the library_name::function path syntax:

tests/test_my_lib.leo
program test_my_lib.aleo {
@test
fn test_double() {
assert_eq(my_lib::double(5u32), 10u32);
}

@test
fn test_triple() {
assert_eq(my_lib::math::triple(4u32), 12u32);
}

@noupgrade
constructor() {}
}

Run leo test from the library's root directory:

cd my_lib
leo test

Submodule functions are accessible through their qualified path (for example, my_lib::math::triple(4u32)).

Running Tests

Invoking the leo test command will run all of the compiled and interpreted tests. Developers may optionally select individual tests by supplying a test function name or a string that is contained within a test function name. For instance, to run the test for test_final, developers would use the following command:

leo test test_final

Either of the following commands will run both of the addition function tests:

leo test simple

or

leo test addition

The results use qualified names such as test_example_program.leo::test_addition. A filter can match this name. It can also match the compiled name test_example_program.aleo/test_addition:

leo test test_example_program.leo::test_addition
leo test test_example_program.aleo/test_addition

See the leo test CLI documentation.

Running a Devnode

leo devnode is a lightweight, single-process node that bypasses consensus and proof generation. It is the recommended local tool for end-to-end deploy/execute testing — no snarkOS installation required.

warning

--skip-deploy-certificate skips both proof generation and the circuit deployment limit check. A deployment that succeeds on a devnode with this flag can still be rejected by Testnet or Mainnet if the circuit exceeds the on-chain limits. Run leo synthesize --local before deploying to a public network to verify your program's constraint count.

See the leo devnode CLI reference for setup instructions, all flags, and a step-by-step workflow.

Running a Devnet

leo devnet starts a full multi-validator snarkOS network locally. It requires more resources than leo devnode but provides a closer approximation of consensus behavior.

See the leo devnet CLI reference for setup instructions and flags.

Deploying/Executing on Testnet

To deploy and execute on Testnet, you will need to set your endpoint back to one of the public facing options. Additionally, you will need to obtain Testnet credits — visit https://faucet.aleo.org/ to request them.

Other Tools

The Aleo community has developed some neat tools to aid in testing.