Skip to the content.

Catadioptre for Java

Table of contents

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).