Testing
- Testing smart contracts in Noir.
- TXE docs needed.
- Testing end-to-end apps.
- See Aztec.js?
This guide shows you how to test your Aztec smart contracts using Noir's TestEnvironment for fast, lightweight testing.
Prerequisites
- An Aztec contract project with functions to test
- Aztec sandbox running (required for
aztec testcommand) - Basic understanding of Noir syntax
Write Aztec contract tests
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.
Run your tests
Execute Aztec Noir tests using:
aztec testTest execution process
- Compile contracts
- Start the sandbox
- Run
aztec test
Basic test structure
use crate::MyContract;
use aztec::{
protocol_types::address::AztecAddress,
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 You can organize tests in separate files:
- Create
src/test.nrwithmod utils;to import helper functions - Split tests into modules like
src/test/transfer_tests.nr,src/test/auth_tests.nr - Import the test module in
src/main.nrwithmod test; - Share setup functions in
src/test/utils.nr:::
Deploying contracts
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");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
}:::
Calling contract functions
TestEnvironment provides methods for different function types:
Private functions
// 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());Public functions
// 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));Utility/Unconstrained functions
// Simulate utility/view functions (unconstrained)
let total = env.simulate_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.simulate_utility(Token::at(token_address).balance_of_private(owner)),
expected
);
}:::
Creating accounts
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
- 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)
}:::
Testing with authwits
Authwits allow one account to authorize another to act on its behalf.
Import authwit helpers
use aztec::test::helpers::authwit::{
add_private_authwit_from_call_interface,
add_public_authwit_from_call_interface,
};Private authwits
#[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_interface(owner, spender, burn_call);
// Spender can now execute the authorized action
env.call_private(spender, burn_call);
}Public authwits
#[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_public(owner, recipient, 100, nonce);
// Grant public authorization
add_public_authwit_from_call_interface(owner, spender, transfer_call);
// Execute with authorization
env.call_public(spender, transfer_call);
}
## Time traveling
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 by manually manipulated by any of the following methods:
```rust
// 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);Testing failure cases
Test functions that should fail using annotations:
Generic failure
#[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());
}Specific error message
#[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));
}Testing authwit failures
#[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);
}