Production Access To A Test-Only Visible Member

ID

java.visiblefortesting_used_in_production

Severity

high

Remediation Complexity

medium

Remediation Risk

low

Remediation Effort

medium

Resource

Reliability

Language

Java

Tags

reliability, testing, visibility

Description

An annotation simply named VisibleForTesting marks a field, method or class whose visibility was relaxed beyond what the API actually needs, purely so a test in the same package can reach it. Guava, AssertJ, Android and Apache Flink all ship one under that exact name, and a home-grown equivalent counts too.

Production code has no such excuse. Once a production class also depends on the relaxed access, the member is effectively public for good: tightening it back to private breaks a caller nobody expects to be maintaining an API contract with.

This rule reports a field read, a method call, or an object allocation made from production code against a member (or class) carrying the annotation, when the member is declared in another file.

super.m(…​) written inside the overriding m is not reported: the annotated member and the caller are the same member, and a subclass reaching its own inherited protected method is ordinary inheritance rather than a new dependency on relaxed visibility.

Rationale

public class OrderPricing {

  @VisibleForTesting
  BigDecimal discountRate;                 // relaxed only so PricingTest can set it directly

  BigDecimal priceFor(Order order) {
    return order.subtotal().multiply(discountRate);
  }
}

public class CheckoutService {

  BigDecimal quote(Order order) {
    OrderPricing pricing = new OrderPricing();
    pricing.discountRate = BigDecimal.ZERO;  // production code reaching into a test-only field
    return pricing.priceFor(order);
  }
}

CheckoutService now depends on discountRate being package-visible. OrderPricing can never go back to hiding that field behind a real API without breaking checkout.

Remediation

Give the caller a real, intentional way to get what it needs — a constructor parameter, a builder option, or a narrowly-scoped public method — instead of reaching through the crack left open for tests.

public class OrderPricing {

  private BigDecimal discountRate;

  @VisibleForTesting
  OrderPricing(BigDecimal discountRate) {   // still test-only, but an explicit contract
    this.discountRate = discountRate;
  }

  OrderPricing() {
    this(BigDecimal.ZERO);
  }
}

Limitations

The annotated member is declared in another file, so the rule resolves it through the project-wide software model. When that model has no entry for the member — an unresolved name, a dependency outside the scanned code, or a scan in which the model was not built — the rule stays silent. Access from the declaring file itself is compliant, matching how the annotation is meant to be used by a same-file test helper. A member whose annotation carries an otherwise attribute — how Android’s androidx.annotation.VisibleForTesting documents a wider intended visibility — is also left unreported, since computing the exact scope that value implies is outside what this rule attempts.

© 2026 Xygeni. All rights reserved