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

Who may depend on test-only code?
Bazel validates every dependency edge during analysis, before execution begins.
All three targets below declare deps = ["//testutil:test_helpers"]. Bazel accepts only consumers that are already on the test side.
TEST-ONLY PRODUCER
//testutil:test_helpers
testonly = True or package(default_testonly = True)
Either declaration marks the producer as test-only.
Consumers declaring a dependency on that producer
PRODUCTION → TEST-ONLY
//app:server
A normal binary or library is not test-only.
REJECTED DURING ANALYSIS
TEST → TEST-ONLY
//app:server_test
A test rule is implicitly test-only.
ALLOWED
TEST-ONLY → TEST-ONLY
//integration:smoke_runner
A non-test rule may set testonly = True.
ALLOWED
If target A depends on test-only code, A must also be a test or test-only. The same check applies again to targets that depend on A.
testonly protects a dependency boundary: production code cannot depend on test-only code, even when visibility permits the label.

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:

Won't build
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.

Reproduce this error

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

key takeaway

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

A non-test target may not depend on a test-only target.
testonly changes whether wildcard patterns select the target.
The restriction is checked while Bazel analyzes dependency edges.

2.Which package declaration makes targets test-only by default?

Select one answer

0 of 2 answered

Footnotes

  1. Preventing production code depending on experiments — using testonly as a dependency boundary, package(default_testonly = True), a concrete error message, and a CI audit with bazel query 1 2 3 4 5 6 7 8

  2. BUILD Style Guide — example BUILD file using package(default_testonly = True) and recommended placement of package() near the top of the file 1 2