This tutorial is written against the Noir documentation snapshot fetched 2026-05-20, specifically the dev docs that also point readers to v1.0.0-beta.21 as the latest version in the navigation. Everything below stays within syntax and concepts that are directly verified by the provided primer, plus standard zero-knowledge engineering practice.
The honest workflow for testing and debugging Noir circuits is:
nargo new and keep circuits small.nargo check early to generate Prover.toml and catch compile issues.nargo execute as your first real test harness: it compiles, executes, and writes a witness.assert(...), because constraint failure is your ground truth.Prover.toml methodically, especially around boundaries and expected failures.The key mindset: in Noir, “testing” is not only about function-level unit tests. It is also about checking whether a set of constraints admits exactly the witnesses you intend, and rejects the ones you do not.
When developers say they want to “test” a Noir circuit, they often mean several different things at once:
The provided Noir primer directly verifies a compact toolchain:
nargo new hello_worldnargo checkProver.tomlnargo executeIt also verifies core language facts that matter for debugging:
pubassert(...)nargo execute generates a witness./target/That is enough to describe a production-useful workflow, even without assuming any undocumented testing framework commands.
Here is the canonical starting point from the primer:
fn main(x: Field, y: pub Field) {
assert(x != y);
}
This tiny circuit already contains the main testing ideas:
xyx != yA valid run means the prover supplied a witness satisfying the constraint. An invalid run means witness generation or execution fails because the constraint system cannot be satisfied under those inputs.
That is the first honest framing: your assertions are your executable specification.
In conventional software, a bug often means “the code returned the wrong value.” In a ZK circuit, a bug often means one of these:
Testing Noir circuits, then, is less about broad runtime observability and more about:
If your local Nargo installation offers additional testing commands, debugger integrations, or richer failure messages, verify against your local installation/compiler version. This tutorial does not assume those APIs.
The smallest reliable workflow begins with project creation.
From the primer:
nargo new hello_world
cd hello_world
nargo check
The primer says nargo check can generate a Prover.toml file, where input values are specified. For the sample circuit:
x = 1
y = 2
Then you run:
nargo execute
The verified behavior is:
nargo execute compiles and executes by default./target/witness-name.gz./target/hello_world.jsonThis gives you a clean three-stage loop:
Use nargo check as the fastest way to catch obvious problems:
Even if your eventual goal is proof generation, you should treat nargo check as the first gate on every change.
nargo execute is where a circuit starts behaving like a tested relation rather than just parsed source code. With one command you learn:
This is the closest verified primitive in the primer to a test runner.
Two files matter immediately:
./target/hello_world.json./target/witness-name.gzThe JSON artifact represents the compiled output of the Noir program. The primer identifies Noir as compiling into ACIR. That means the artifact is part of the debugging surface: it tells you there is a concrete compiled circuit, not just source code.
The witness file matters because many ZK bugs are not source-level syntax bugs. They are witness-generation or constraint-satisfaction bugs. The fact that nargo execute emits a witness is evidence that, for that concrete input assignment, the relation was satisfiable.
For any nontrivial circuit, keep at least one Prover.toml assignment that you know should succeed. Whenever you change the circuit:
nargo checknargo execute with that known-good inputThis helps distinguish:
For the sample circuit:
fn main(x: Field, y: pub Field) {
assert(x != y);
}
You should test at least these two cases:
Valid:
x = 1
y = 2
Invalid:
x = 1
y = 1
The first case should execute successfully. The second should fail because the assertion is not satisfied.
That sounds trivial, but it is exactly the discipline that scales. Every important assertion in a circuit should have at least:
If you do only happy-path runs, you are not really testing a circuit. You are only showing that at least one witness exists.
A circuit that is hard to test is usually a circuit whose intended relation is not stated clearly enough. In Noir, the primary verified mechanism for stating correctness conditions in the primer is assert(...).
That leads to an honest recommendation: break correctness into small, explicit invariants.
Start again from the verified pattern:
fn main(x: Field, y: pub Field) {
assert(x != y);
}
What makes this testable?
Now compare that to a more realistic development mistake: combining several intended checks mentally, but writing only one of them in code. In a ZK circuit, anything not constrained is effectively unconstrained. Standard ZK engineering practice treats this as one of the most dangerous bug classes.
So when you design a Noir circuit:
If the protocol requires several properties, constrain each property directly rather than assuming one condition implies another unless that implication is obvious and deliberate.
Even with very simple syntax, you can encode separate invariants one by one. For example:
fn main(x: Field, y: pub Field) {
assert(x != y);
assert(x != 0);
}
This example uses only documented Noir syntax patterns already visible in the primer:
Fieldpub Fieldassert(...)The exact availability of every operation or literal handling should be normal in Noir, but for any nuance beyond the primer, verify against your local installation/compiler version.
The point is conceptual: if x must be different from y, and also must be nonzero, make both requirements explicit.
The primer is very clear:
pubThis has direct debugging consequences.
If you are confused about a circuit’s expected outputs or externally visible commitments, first ask:
pub?A large class of integration bugs is not “the math is wrong,” but “the visibility contract is wrong.”
For example:
fn main(secret: Field, expected: pub Field) {
assert(secret != expected);
}
Here the verifier learns expected, not secret. That is easy to reason about in a test because you can vary the private witness separately from the public statement.
main tinyFor debugging, the best Noir programs often begin life as almost embarrassingly small circuits. A small main gives you three benefits:
If a circuit is becoming difficult to reason about, do not immediately reach for tool-specific debugging features. First ask whether the relation can be restated as a smaller circuit with one or two assertions that isolate the issue.
That is often faster and more reliable.
Prover.toml as a real test harnessThe primer verifies that Prover.toml is where input values are specified. Many Noir developers treat it as a temporary file just to get a demo running. That is a missed opportunity.
Used carefully, Prover.toml is your first real test harness.
For the sample program:
fn main(x: Field, y: pub Field) {
assert(x != y);
}
you can drive several meaningful cases by changing only the input file.
x = 1
y = 2
This shows that the circuit admits a valid witness for a straightforward satisfying assignment.
x = 1
y = 1
This shows that the same circuit rejects an assignment violating the intended relation.
x = 5
y = 7
This helps you reason about how the statement seen by the verifier changes while preserving the same structure.
x = 8
y = 7
This helps you distinguish “the public statement changed” from “only the prover’s witness changed.”
That distinction matters because public and private values have different protocol meaning even when they look identical in source code.
For any nontrivial circuit, keep a short table of input classes:
The primer does not define a dedicated Noir test file format or parameterized test API in the provided excerpts, so the portable workflow is to manage these input sets explicitly and rerun nargo execute.
If your local workflow includes scripts, CI jobs, or additional Nargo subcommands, that can be effective, but verify against your local installation/compiler version.
In standard application testing, it is often enough to validate output values. In a circuit, you are also validating the existence or nonexistence of satisfying witnesses.
That means a good test plan includes both:
Negative tests are especially important because under-constrained circuits tend to fail silently in design, not at syntax level. If you forgot an assertion, a bad input may still execute successfully. That is the exact behavior you are trying to detect.
For each Prover.toml case, write down:
Without that discipline, “debugging” turns into trial and error.
When a Noir circuit does not behave as expected, there are only a few honest possibilities:
Prover.toml are wrongThe best debugging workflow follows that order.
Suppose a larger circuit fails during nargo execute. Your first move should not be to add more complexity. It should be to remove as much as possible while preserving the failure.
For example, if you suspect the issue is in a single inequality, create a minimal circuit:
fn main(x: Field, y: pub Field) {
assert(x != y);
}
Then reproduce the problem with just those inputs.
This technique is standard engineering practice, but it is especially effective in ZK systems because constraint systems can be difficult to reason about globally. A small reproducer lets you verify whether the issue is:
If execution succeeds but the protocol semantics are wrong, ask whether the issue is actually about pub.
The primer distinguishes very clearly between:
Do not mix these ideas.
When debugging application-level behavior, it is often useful to temporarily simplify the circuit so that one input is clearly public and one clearly private. The sample pattern does exactly that:
fn main(x: Field, y: pub Field) {
assert(x != y);
}
Now you can reason about whether the externally known statement is the one you meant to expose.
The primer states that nargo execute automatically compiles if the program was not already compiled or was edited, and writes compiled artifacts under ./target/hello_world.json.
That means artifact awareness is part of an honest workflow. If behavior seems stale or confusing:
The primer does not document cache-clearing or advanced build options in the provided excerpts, so avoid assuming them here.
A successful witness generation means: for the given program and inputs, the relation was satisfiable.
A failed execution means: at least one assertion or constraint prevented witness generation for those inputs.
This binary signal is more informative than it seems.
If a negative case unexpectedly succeeds, the usual explanation is not “the prover cheated.” It is much more likely that the circuit did not constrain what you thought it constrained.
If a positive case unexpectedly fails, the usual explanation is one of:
Before changing code, write one sentence describing the intended relation.
For the basic sample:
The prover knows a private
xsuch thatxis different from the publicy.
Then compare that sentence directly to the code:
fn main(x: Field, y: pub Field) {
assert(x != y);
}
If those do not match exactly, you have found the bug or at least the ambiguity.
This sounds simple, but many circuit bugs come from a drift between prose protocol definitions and encoded constraints. An honest workflow keeps both visible during debugging.
Even with only the verified Noir tooling in the primer, you can run a disciplined workflow that is suitable for serious evaluation.
A practical project layout starts from what nargo new gives you:
src/main.nrNargo.tomlThe primer also introduces Prover.toml. Treat that as part of the circuit’s executable documentation, not just a temporary local file.
For each important circuit state, keep:
During code review, every assert(...) should answer:
That is a more useful review discipline than only asking whether the code “looks right.”
A single successful nargo execute proves only that one witness exists for one input assignment.
It does not prove:
The primer explicitly says that proving and verification require a proving backend such as Barretenberg. So a prudent workflow separates:
This tutorial focuses on the first stage because that is what the provided material verifies directly.
This tutorial is written against the Noir docs snapshot fetched 2026-05-20, referencing the dev docs and their navigation mention of v1.0.0-beta.21.
That matters because tooling around testing and debugging evolves quickly in ZK ecosystems. If you see examples elsewhere using commands, flags, or APIs not shown in the provided primer, do not assume they are available in your setup. Verify against your local installation/compiler version.
Sometimes the circuit is behaving correctly, and the real bug is in the protocol assumption.
A few warning signs:
At that point, more CLI work will not rescue the design. Rewrite the relation, then rewrite the circuit.
This tutorial intentionally avoids undocumented Noir APIs.
The provided primer does not verify a dedicated nargo test command, test file conventions, tracing macros, or advanced debug output. If your local toolchain supports them, verify against your local installation/compiler version.
Code examples are deliberately minimal.
They use only syntax that is directly supported by the primer’s examples and language discussion: fn main(...), Field, pub Field, and assert(...).
A successful execution is not a proof-system integration test.
The primer distinguishes Noir compilation/witness generation from proving and verification, which require a backend such as Barretenberg.
Public/private mistakes are common and not always obvious.
The circuit may be logically correct yet still violate application privacy requirements if the wrong values are marked pub.
Negative tests are not optional.
In ZK development, a circuit that only passes valid examples may still be under-constrained. You need examples that should fail.
Artifact behavior can vary by version.
The primer documents ./target/hello_world.json and ./target/witness-name.gz, but file naming, exact formats, and surrounding tooling may change. Verify against your local installation/compiler version.
Noir Quick Start
https://noir-lang.org/docs/dev/getting_started/quick_start
Noir Data Types
https://noir-lang.org/docs/dev/noir/concepts/data_types/
Noir documentation root
https://noir-lang.org/docs/dev/
Barretenberg getting started entry point referenced by Noir docs
https://barretenberg.aztec.network/
ACIR reference entry point from Noir docs navigation
https://noir-lang.org/docs/dev/ (navigate to ACIR reference in the docs sidebar)
Thanks for reading this far. If “Noir testing and debugging” connected with where you are, three concrete next steps:
The full Midnight ZK Cookbook index has 17 tutorials across Midnight, Aleo, Aztec, Noir, and risc0 plus 4 Chinese translations. Adjacent tutorials are listed by ecosystem on that page.
Bounty Radar tracks open ZK bounties across Algora, GitHub labels, Drips Wave, Code4rena, and Bountycaster. Browse the Noir sub-feed; JSON at /noir.json. The free tier is poll-based; the $19/mo Hobbyist tier pushes one filter to your Telegram in real time.
zk-pipeline-doctor is the free MIT-licensed CLI that scores any ZK project on tests, CI, docs, security, reproducibility, and language toolchain (supports Compact, Leo, Noir, Cairo, and 7 Rust zkVMs). Drop it into a GitHub Action with zk-doctor-action for diff-aware PR comments. The $15/mo Pro tier adds four cross-ecosystem deep detectors (circuit complexity, proving-system pitfalls, verifier soundness, multi-file consistency).
Drafted with AI assistance and reviewed by the author before publishing. See DISCLOSURE for the full process.
Three ways to support this work, pick whichever matches your situation:
Free alternative: Sponsor on GitHub · Star the repo · Share with one ZK developer who'd benefit