Catadioptre for Java
Table of contents
- Import the dependencies
- Generate and use proxy methods to access your private members in tests
- Setting a private or protected field
- Getting a private or protected field
- Executing a private or protected method
Import the dependencies
You can directly get the dependency from Maven Central.
With Gradle and the Groovy DSL
testImplementation 'io.aeris-consulting:catadioptre-java:0.4.1'
// For the code generation.
compileOnly 'io.aeris-consulting:catadioptre-annotations:0.4.1'
With Gradle and the Kotlin DSL
testImplementation("io.aeris-consulting:catadioptre-java:0.4.1")
// For the code generation.
compileOnly("io.aeris-consulting:catadioptre-annotations:0.4.1")
With Maven
<dependency>
<groupId>io.aeris-consulting</groupId>
<artifactId>catadioptre-java</artifactId>
<version>0.4.1</version>
<scope>test</scope>
</dependency>
<!-- For the code generation, use the following. -->
<dependency>
<groupId>io.aeris-consulting</groupId>
<artifactId>catadioptre-java</artifactId>
<version>0.4.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.aeris-consulting</groupId>
<artifactId>catadioptre-annotations</artifactId>
<version>0.4.1</version>
<scope>provided</scope>
</dependency>
You can find more on Maven Central.
Generate and use proxy methods to access your private members in tests
Configure the build
To facilitate the access to the private members in a test context, Catadioptre generates for you static methods, that route the calls to the private members using reflection.
Whereas those methods are meant to be only used in a testing context, you can use them for production by adapting the configuration documented below.
Those methods are generated by an annotation processor that requires the catadioptre-annotations
module to be in the
compilation classpath. See here how to add the required dependency.
To include the generated classes into the test sources, configure your project as follows:
With Gradle and the Groovy DSL
java.sourceSets["test"].java.srcDir(layout.buildDirectory.dir("generated/sources/annotationProcessor/java/catadioptre"))
With Gradle and the Kotlin DSL
java.sourceSets["test"].java.srcDir(layout.buildDirectory.dir("generated/sources/annotationProcessor/java/catadioptre"))
With Maven
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.2.0</version>
<executions>
<execution>
<id>add-test-source</id>
<phase>generate-test-sources</phase>
<goals>
<goal>add-test-source</goal>
</goals>
<configuration>
<sources>
<source>target/generated-sources/catadioptre</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
Annotate the “invisible” code to test
Then, simply add the @Testable
annotation on the private members and compile the class:
public class CatadioptreExample {
@Testable
private final Map<String, Double> markers;
public CatadioptreExample(final Map<String, Double> markers) {
this.markers = markers;
}
@Testable
private Double multiplySum(double multiplier, Double... valuesToSum) {
return Arrays.stream(valuesToSum).filter(Objects::nonNull).mapToDouble(d -> d).sum() * multiplier;
}
}
Finally, use the generated methods on your tests:
public class CatadioptreExampleTest {
@Test
public void useProxiesInTest() {
// First create your instance.
Map<String, Double> defaultProperty = new HashMap<>();
defaultProperty.put("any", 1.0);
CatadioptreExample instance = new CatadioptreExample(defaultProperty, 1.0, Optional.empty());
// Read an annotated variable.
Map<String, Double> result = TestableCatadioptreExample.markers(instance);
// Write an annotated variable.
defaultProperty = new HashMap<>();
defaultProperty.put("other", 2.0);
CatadioptreExample result = TestableCatadioptreExample.markers(instance, defaultProperty);
// Execute an annotated method.
double result = TestableCatadioptreExample.multiplySum(instance, 2.0, new Double[]{1.0, 3.0, 6.0});
}
}
Limitations on the generation of proxy methods for Java with Gradle
Since the generated code is not part of the standard output of the java compilation task, it cannot be cached. Hence, running after you execute the clean task which will remove the generated code, simply running the compilation again will not regenerate it, because the classes will be restored from the cache.
To bypass it, you have to compile the Java code using the --rerun-tasks
options.
We are working to provide you a more convenient solution in the future.
Further examples
This repository contains three different folders to demo the full configuration and usage of Catadioptre, using Gradle ( with Groovy or Kotlin DSL) and Maven. You can run this examples locally to see how the whole is working:
Setting a private or protected field
Writing a value into a field can be performed with the static method ReflectionFieldUtils.setField
on any instance. The function takes the
instance owning the value as first argument, the name of the property as second and finally the value to set.
ReflectionFieldUtils.setField(instance, "myProperty", 456);
While you can use setField
to set a field to null
, a more concise option consists in using the
method ReflectionFieldUtils.clearField
:
ReflectionFieldUtils.clearField(instance, "myProperty");
Getting a private or protected field
To read the current value of a field, you can use the static function ReflectionFieldUtils.getField
expecting the instance and the field name as arguments.
int result = ReflectionFieldUtils.getField(instance, "myProperty");
Executing a private or protected method
Executing a method is extremely simple and requires to pass the instance, the name of the method and the list of arguments.
double result = ReflectionMethodUtils.executeInvisible(object, "divideSum", 2, Argument.ofVarargs(Integer.class, 1, 3, 6));
Note that in the example above, when you need to pass a variable argument, you will have to use the relevant facility
on the class Argument
.
Argument
also allows you to specify the type of null arguments, in order to find the convenient method to be used in case of
polymorphism: Argument.ofNull(TheArgument.class)
.