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.