Aztec QuickstartSDK
← Quest map

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:

noir
#[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));
}
bash
npm run test:contracts   # aztec test (TXE)

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:

typescript
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:

typescript
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);
});
bash
npm run test:integration   # vitest, in-process network
# or run both tiers:
npm test