★ Test the app
Not required to ship, but worth knowing once the contract compiles: Aztec gives you two tiers of test, and the app uses both. npm test runs them back to back.
TXE unit tests#
The TXE (Test eXecution Environment) runs your contract functions directly — no network, no proving — so the loop is fast. The voting contract's tests live in their own crate at packages/contracts/test/src/lib.nr and cover the pedagogy head-on: a vote bumps the tally, two different accounts both count, end_vote is admin-gated, and — the important one — a second vote from the same account fails on a duplicate nullifier. A TXE test is plain Noir: call the contract as each account with env.call_public / env.call_private, and pin the expected failure with should_fail_with:
#[test(should_fail_with = "duplicate siloed nullifier")]
unconstrained fn test_fail_vote_twice() {
let (mut env, voting_contract_address, admin) = setup();
let alice = env.create_light_account();
let election_id = ElectionId::new(Field::from(42));
env.call_public(admin, PrivateVoting::at(voting_contract_address).start_vote(election_id));
let candidate = 101;
env.call_private(alice, PrivateVoting::at(voting_contract_address).cast_vote(election_id, candidate));
env.call_private(alice, PrivateVoting::at(voting_contract_address).cast_vote(election_id, candidate));
}npm run test:contracts # aztec test (TXE)Run the contract's TXE unit tests with npm run test:contracts, then open packages/contracts/test/src/lib.nr in my editor and walk me through the tests — especially the one where a second vote from the same account fails with "duplicate siloed nullifier" — and explain what the TXE gives us that a full network run does not.
Integration test#
The integration suite in test/integration/voting.test.ts drives the real contract through a local Aztec network the test spins up itself — exactly the REGISTER → SIMULATE → SEND flow the frontend runs, but headless and fast. Its setup is the whole network story in one place: the SDK's setupLocalNetwork (from @aztec/aztec/testing) runs the node inline in the test process, backed by a throwaway anvil L1 it spawns per suite — the same codepath as aztec start --local-network, with nothing to start by hand. Then it creates a prefunded account, deploys the contract, and opens the election:
beforeAll(async () => {
// Prefund the first test account at genesis; as an initializerless account it
// needs no deploy tx — creating it registers the instance and it's usable.
const [testAccount] = await getInitialTestAccountsData();
admin = testAccount.address;
network = await setupLocalNetwork({ fundedAddresses: [admin] });
wallet = await EmbeddedWallet.create(network.node, { ephemeral: true });
await createFundedInitializerlessAccounts(wallet, [testAccount]);
// Deploy PrivateVoting (registers class + instance + runs constructor) and
// open the election. `deploy` also registers the instance with our PXE.
const deployMethod = PrivateVotingContract.deploy(wallet, admin, {
deployer: admin,
salt: new Fr(0),
});
await wallet.registerContract(await deployMethod.getInstance(), PrivateVotingContractArtifact);
const { contract } = await deployMethod.send({ from: admin });
voting = contract;
await voting.methods.start_vote(ELECTION).send({ from: admin });
}, 300_000);
afterAll(async () => {
await network?.stop();
});Each test then talks to the deployed contract like the frontend does. The tally test below is SIMULATE → SEND → read; the rest of the suite asserts the public TallyUpdated event, the private Vote event, and the duplicate-vote rejection end to end:
it("counts a private vote in the public tally", async () => {
// SIMULATE then SEND — the same flow the frontend runs.
await voting.methods.cast_vote(ELECTION, ALICE_PICK).simulate({ from: admin });
await voting.methods.cast_vote(ELECTION, ALICE_PICK).send({ from: admin });
const { result } = await voting.methods.get_tally(ELECTION, ALICE_PICK).simulate({ from: admin });
expect(BigInt(result.toString())).toBe(1n);
});npm run test:integration # vitest, in-process network
# or run both tiers:
npm testRun npm run test:integration and explain how the in-process network test in test/integration/voting.test.ts differs from the TXE unit tests — what it exercises that the TXE cannot (deployment, events, the real send flow), and where its network comes from (setupLocalNetwork from @aztec/aztec/testing).