1.2.4 testonly Attribute
testonly marks a dependency boundary. It lets you keep test helpers or experimental code in the repo while still preventing production targets from taking a dependency on them.1 Bazel enforces the rule when it validates the dependency graph, so the failure shows up during analysis rather than after the build has already run.1
What testonly changes
A testonly target can still be public, can still use ordinary labels, and can still participate in the same deps graph. What changes is the kind of consumer Bazel will accept. In practical terms, once a dependency edge crosses into test-only code, the consumer has to stay on the test side of the boundary too.1 The formal graph vocabulary comes later in 2.1.1 Nodes, Edges & Acyclicity.
That makes testonly a different tool from 0.2.4 Visibility. Visibility decides which packages may depend on a target. testonly decides whether production code may depend on it at all. The package-wide form from 0.2.6 package() Function is often the easiest way to apply the policy to whole directories.1,2
What the failure looks like
The testonly-error snippet keeps the setup small. The helper lives in a package with a default test-only policy:
package(default_testonly = True)
java_library(
name = "test_helpers",
srcs = ["TestHelpers.java"],
visibility = ["//visibility:public"],
)
A production binary then tries to depend on it:
java_binary(
name = "server",
srcs = ["Main.java"],
main_class = "app.Main",
deps = ["//testutil:test_helpers"],
)
Running bazel build //app:server produces a real analysis error:
ERROR: .../app/BUILD.bazel:3:12: in java_binary rule //app:server: non-test target '//app:server' depends on testonly target '//testutil:test_helpers' and doesn't have testonly attribute set
ERROR: .../app/BUILD.bazel:3:12: Analysis of target '//app:server' (config: b96f21e) failed
ERROR: Analysis of target '//app:server' failed; build aborted
Bazel points at the consumer because that is where the illegal edge was declared. The helper is deliberately //visibility:public in the snippet, and the build still fails. visibility and testonly stack. Making a target publicly visible does not make it safe for production dependencies.
Mark packages, then audit exceptions
For directories that are supposed to stay on the test side of the boundary, package(default_testonly = True) is usually better than repeating testonly = True on every target.1,2 It fits directories like //testutil or //experimental, where the policy is about the whole package, not one special rule.1
The default is not absolute, though. A target can still opt out with testonly = False, which means package defaults are a convenience mechanism, not a complete policy by themselves.1
If a directory is meant to remain test-only, add a cheap CI audit with bazel query:
bazel query 'attr(testonly, 0, //experimental/...)'
Any output means Bazel found a target under that subtree that is not test-only. At Level 1, this is enough: reuse the query basics from 1.1.8 Basic Query. The richer auditing patterns come later in 5.2 Query.1
Use testonly when code should stay reusable from tests but unavailable to production dependencies. Mark either the target or the whole package, let Bazel reject illegal edges during analysis, and add a small bazel query guard if a subtree must stay test-only over time.
Check your understanding · 2 questions
1.True or false: testonly dependency boundaries.
Choose True or False for each sentence
2.Which package declaration makes targets test-only by default?
Select one answer
Footnotes
-
Preventing production code depending on experiments — using
testonlyas a dependency boundary,package(default_testonly = True), a concrete error message, and a CI audit withbazel query↩1 ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 -
BUILD Style Guide — example
BUILDfile usingpackage(default_testonly = True)and recommended placement ofpackage()near the top of the file ↩1 ↩2