| title | Testing Contracts | ||||
|---|---|---|---|---|---|
| tags |
|
||||
| keywords |
|
||||
| sidebar_position | 6 | ||||
| description | Write and run tests for your Aztec smart contracts using Noir's TestEnvironment. |
This guide shows you how to test your Aztec smart contracts using Noir's TestEnvironment for fast, lightweight testing.
- An Aztec contract project with functions to test
- Basic understanding of Noir syntax
:::tip For complex cross-chain or integration testing, see the TypeScript testing guide. :::
Use TestEnvironment from aztec-nr for contract unit testing:
- Fast: Lightweight environment with mocked components
- Convenient: Similar to Foundry for simple contract tests
- Limited: No rollup circuits or cross-chain messaging
For complex end-to-end tests, use TypeScript testing with aztec.js.
Execute Aztec Noir tests using:
aztec test- Compile contracts
- Run
aztec test
:::warning
Always use aztec test instead of nargo test. The TestEnvironment requires the test environment oracle resolver provided by the aztec CLI.
:::
Tests live in the same crate as your contract. aztec new creates a single-crate project, and the convention is to place #[test] functions in a mod tests block alongside the contract (or in submodules of the crate):
use aztec::macros::aztec;
#[aztec]
pub contract MyContract {
// ...contract functions...
}
mod tests {
use super::MyContract;
use aztec::test::helpers::test_environment::TestEnvironment;
#[test]
unconstrained fn test_basic_flow() {
// 1. Create test environment
let mut env = TestEnvironment::new();
// 2. Create accounts
let _owner = env.create_light_account();
}
}:::info Test execution notes
- Tests run in parallel by default
- Use
unconstrainedfunctions for faster execution - See all
TestEnvironmentmethods here
:::
:::tip Organizing test files
For larger test suites, split tests into submodules of your crate rather than keeping them all inside main.nr:
- Create modules like
src/transfer_tests.nr,src/auth_tests.nr - Declare them from
src/main.nrwithmod transfer_tests;,mod auth_tests; - Share setup functions in
src/test_utils.nr
See the aztec-standards token contract for a worked example of this layout. :::
In order to test you'll most likely want to deploy a contract in your testing environment. First, instantiate a deployer:
let deployer = env.deploy("ContractName");
// If on a different crate:
let deployer = env.deploy("../other_contract");:::warning
It is always necessary to deploy a contract in order to test it. aztec test automatically compiles contracts when changes are detected, but you can also manually compile with aztec compile to regenerate the bytecode and ABI.
:::
You can then choose whatever you need to initialize by interfacing with your initializer and calling it:
let initializer = MyContract::interface().constructor(param1, param2);
let contract_address = deployer.with_private_initializer(owner, initializer);
let contract_address = deployer.with_public_initializer(owner, initializer);
let contract_address = deployer.without_initializer();:::tip Reusable setup functions Create a setup function to avoid repeating initialization code:
pub unconstrained fn setup(initial_value: Field) -> (TestEnvironment, AztecAddress, AztecAddress) {
let mut env = TestEnvironment::new();
let owner = env.create_light_account();
let initializer = MyContract::interface().constructor(initial_value, owner);
let contract_address = env.deploy("MyContract").with_private_initializer(owner, initializer);
(env, contract_address, owner)
}
#[test]
unconstrained fn test_something() {
let (env, contract_address, owner) = setup(42);
// Your test logic here
}:::
TestEnvironment provides methods for different function types:
// Call private function
env.call_private(caller, Token::at(token_address).transfer(recipient, 100));
// Returns the result
let result = env.call_private(owner, Contract::at(address).get_private_data());// Call public function
env.call_public(caller, Token::at(token_address).mint_to_public(recipient, 100));
// View public state (read-only)
let balance = env.view_public(Token::at(token_address).balance_of_public(owner));// Simulate utility/view functions (unconstrained)
let total = env.execute_utility(Token::at(token_address).balance_of_private(owner));:::tip Helper function pattern Create helper functions for common assertions:
pub unconstrained fn check_balance(
env: TestEnvironment,
token_address: AztecAddress,
owner: AztecAddress,
expected: u128,
) {
assert_eq(
env.execute_utility(Token::at(token_address).balance_of_private(owner)),
expected
);
}:::
Two types of accounts are available:
// Light account - fast, limited features
let owner = env.create_light_account();
// Contract account - full features, slower
let owner = env.create_contract_account();:::info Account type comparison Light accounts:
- Fast to create
- Work for simple transfers and tests
- Cannot process authwits
- No account contract deployed
Contract accounts:
- Required for authwit testing
- Support account abstraction features
- Slower to create (deploys account contract)
- Needed for cross-contract authorization :::
:::tip Choosing account types
pub unconstrained fn setup(with_authwits: bool) -> (TestEnvironment, AztecAddress, AztecAddress) {
let mut env = TestEnvironment::new();
let (owner, recipient) = if with_authwits {
(env.create_contract_account(), env.create_contract_account())
} else {
(env.create_light_account(), env.create_light_account())
};
// ... deploy contracts ...
(env, owner, recipient)
}:::
Authwits allow one account to authorize another to act on its behalf.
:::warning Authwits require contract accounts, not light accounts. :::
use aztec::test::helpers::authwit::{
add_private_authwit_from_call,
add_public_authwit_from_call,
};#[test]
unconstrained fn test_private_authwit() {
// Setup with contract accounts (required for authwits)
let (env, token_address, owner, spender) = setup(true);
// Create the call that needs authorization
let amount = 100;
let nonce = 7; // Non-zero nonce for authwit
let burn_call = Token::at(token_address).burn_private(owner, amount, nonce);
// Grant authorization from owner to spender
add_private_authwit_from_call(env, owner, spender, burn_call);
// Spender can now execute the authorized action
env.call_private(spender, burn_call);
}#[test]
unconstrained fn test_public_authwit() {
let (env, token_address, owner, spender) = setup(true);
// Create public action that needs authorization
let transfer_call = Token::at(token_address).transfer_in_public(owner, recipient, 100, nonce);
// Grant public authorization
add_public_authwit_from_call(env, owner, spender, transfer_call);
// Execute with authorization
env.call_public(spender, transfer_call);
}Contract calls do not advance the timestamp by default, despite each of them resulting in a block with a single transaction. Block timestamp can instead be manually manipulated by any of the following methods:
// Sets the timestamp of the next block to be mined, i.e. of the next public execution. Does not affect private execution.
env.set_next_block_timestamp(block_timestamp);
// Same as `set_next_block_timestamp`, but moving time forward by `duration` instead of advancing to a target timestamp.
env.advance_next_block_timestamp_by(duration);
// Mines an empty block at a given timestamp, causing the next public execution to occur at this time (like `set_next_block_timestamp`), but also allowing for private execution to happen using this empty block as the anchor block.
env.mine_block_at(block_timestamp);Test functions that should fail using annotations:
#[test(should_fail)]
unconstrained fn test_unauthorized_access() {
let (env, contract, owner) = setup(false);
let attacker = env.create_light_account();
// This should fail because attacker is not authorized
env.call_private(attacker, Contract::at(contract).owner_only_function());
}#[test(should_fail_with = "Balance too low")]
unconstrained fn test_insufficient_balance() {
let (env, token, owner, recipient) = setup(false);
// Try to transfer more than available
let balance = 100;
let transfer_amount = 101;
env.call_private(owner, Token::at(token).transfer(recipient, transfer_amount));
}#[test(should_fail_with = "Unknown auth witness for message hash")]
unconstrained fn test_missing_authwit() {
let (env, token, owner, spender) = setup(true);
// Try to burn without authorization
let burn_call = Token::at(token).burn_private(owner, 100, 1);
// No authwit granted - this should fail
env.call_private(spender, burn_call);
}The test environment uses an oracle interface to communicate between your Noir test code and the aztec test CLI. This interface is versioned so that mismatches between the Aztec.nr dependency used to compile the test and the CLI version are detected automatically.
The version uses two components, major.minor, with the same compatibility rules as PXE oracle versioning:
majormust match exactly. A major bump means oracles were removed or had their signatures changed, and a test environment on a different major cannot safely run the test.minorindicates additive changes (new oracles). The test environment uses a best-effort approach: a test compiled against a higherminoris still allowed to run, and an error is only thrown if the test actually invokes an oracle the test environment does not know about.
If you see an error like "Incompatible test environment version: The test was compiled with a newer version of Aztec.nr than your test environment supports", the test uses oracles from a newer Aztec.nr than your aztec test CLI supports.
To fix it, make sure your aztec CLI version and the aztec dependency in the test crate's Nargo.toml are on the same release. Note that the test crate's Aztec.nr version can differ from the contract crate's version, depending on your project configuration. For example, if your CLI is on v4.3.0, the test crate's Nargo.toml should reference the matching tag:
[dependencies]
aztec = { git="https://github.com/AztecProtocol/aztec-nr", tag="v4.3.0", directory="aztec" }If the test environment reports a version that should include every oracle the test needs but an oracle is still missing, this is likely a bug rather than a version problem.