It is generally a good practice to use the most restrictive access level that makes sense. However, Java’s access control is not always as expressive as one would want. In this blog post, I am going to give two examples where an access level must be artificially relaxed. Then, I’ll describe Wealthfront’s recently open sourced solution to this problem.
First, let’s have a look at Guava‘s Lists class. Some of its factory methods rely on the internal static method computeArrayListCapacity(int), which calculates the capacity of an ArrayList. The perfect access level modifier for this method is private, as the method is not meant to be used by other classes.
Unfortunately, declaring this internal method as private would hinder its testability. Indeed, one would need to test the computeArrayListCapacity(int) method indirectly by invoking one of the factory methods in Lists. The resulting test would be hard to understand (as the tested method is not clearly visible in the test) and fragile (as the test is now tied to the implementation of the invoked method.)
Consequently, the visibility of the computeArrayListCapacity(int) method must be artificially relaxed to package-private. In order to document this decision, the method is annotated with Guava’s @VisibleForTesting annotation. Hopefully, this annotation will discourage external users to rely on the method, which is what the compiler would do for a private method.
Another example of the lack of expressivity in Java’s access control occurs when using a dependency injection framework such as Guice. Injected dependencies should never be manually assigned outside tests. For instance, the following class
should never be used like this in production code
On the other hand, this class is meant to be used exactly like this in tests. Hence, the processor and transactionLog fields should behave as private members in production code, but as package-private members in tests. As with the @VisibleForTesting annotation, the @Inject annotation acts as a cue for external users not to rely on the annotated fields in production code.
In both examples, annotations are used as a mean to extend Java’s access control (among other things, obviously.) Unfortunately, the compiler can’t help us control access to the annotated members. That being said, we can write a test that does exactly that, and that’s what we did. We call it the Visibility Test, and we open sourced it.
As you can see in this example, the Visibility Test is configured by specifying the intent of an annotation. Here, we want everything in bin/ annotated with @VisibleForTesting and @Inject to behave as private. The VisibilityTestRunner uses ASM to perform the following two passes on our class files:
- Find all classes, fields, constructors and methods annotated with the specified annotations.
- Find all instructions referring to these classes, fields, constructors and methods.
During the second pass, the runner records illegal accesses and reports them.
You can start using this test today by installing the kawala-testing sub-project available on GitHub.