Vladimir Bychkov's Blog

Java Reflections unit-testing

Reflections Use Cases

Using Reflections in Java is considered bad manners. And rightly so: using reflections with third-party code can lead to data corruption and errors that are difficult to reproduce. However, sometimes it is very difficult to avoid its use. Here are some examples of such cases.

1) Java Persistence Query Language

Many examples can be found in the Jakarta EE tutorial, a chapter on JPQL. Snapshot 1 is one of them.

public List findWithName(String name) {
return em.createQuery(
    "SELECT c FROM Customer c WHERE c.name LIKE :custName")
    .setParameter("custName", name)
    .setMaxResults(10)
    .getResultList();
}

Snapshot 1 — JPQL example

Java Persistence Query Language is similar to SQL and is therefore easy to learn and use. It is this API, and not the Criteria API, that is most often used for databases queries.

2) Expressions in markup

For example Jakarta Faces markup uses Expression language, as Snapshot 2 shows.

<h:inputText id="inputGuess"
    value="#{userNumberBean.userNumber}"
    required="true" size="3"
    disabled="#{userNumberBean.number eq userNumberBean.userNumber ...}"
    validator="#{userNumberBean.validateNumberRange}">
</h:inputText>

Snapshot 2 — Example of usage expression language in Jakarta Faces

Different expression languages (JSR-341, Velocity, FreeMarker and many other) widely use expressions with POJO fields.

3) The established practice

public void configure() throws Exception {
    from("direct:start").bean("beanName", "methodName");
}

Snapshot 3 — Apache Camel example

Some frameworks allow the use of Reflections and developers are actively exploiting this feature. Snapshot 2 is an example from the Apache Camel documentation that uses Reflections to specify a method that should be called.

Unit testing reflections

In both of the above cases, properties or methods of objects are accessed by their name. The JVM compiler does not monitor these accesses. The IDE also does not help in this situation. If the name of a property or method changes, accessing a nonexistent class member will acquire a runtime error. It is quite difficult to prevent such an error from occurring: each use of reflections must be verified by an appropriate unit test. For example, a unit test for JPQL might look like snapshot 3.

@Test
public void testField() {
	Class<?> targetClass = Customer.class;
	String fieldName = "name";
	Class<?> fieldType = String.class;
	
	try {
		Field field = targetClass.getDeclaredField(fieldName);
		Assertions.assertEquals(fieldType, field.getType());
	} catch (NoSuchFieldException | SecurityException e) {
		try {
			Field field = targetClass.getSuperclass().getDeclaredField(fieldName);
			Assertions.assertEquals(fieldType, field.getType());
		} catch (NoSuchFieldException | SecurityException e1) {
			Assertions.fail(e);
		}
	}
}

Snapshot 4 — JUnit-test for JPQL example (see snapshot 1)

The code text can be shortened using such utilities as Apache Commons, Spring Framework or JUnit API. But it may require a significant amount of such unit tests. Writing them requires some discipline from the developer and can be a cumbersome task.

Automating the creation of unit tests for reflections

Here’s a way to automate the creation of such unit tests. Instead of writing a unit test, use the annotation in the same code as reflections. Adding an annotation automatically creates a unit test

@CheckField(targetClass=Customer.class, value="name", type=String.class)
public List findWithName(String name) {
return em.createQuery(
    "SELECT c FROM Customer c WHERE c.name LIKE :custName")
    .setParameter("custName", name)
    .setMaxResults(10)
    .getResultList();

Snapshot 5 — JPQL example (see snapshot 1) with autogenerating JUnit-test annotation

@CheckMethod(targetClass=ExampleBean.class, value="methodName")
public void configure() throws Exception {
    from("direct:start").bean("beanName", "methodName");
}

Snapshot 6 — Apache Camel example (see snapshot 3) with autogenerating JUnit-test annotation

Examples 5 and 6 show the use of @CheckField and @CheckMethod annotations. The @CheckConstructor annotation works in a similar way. Together, these annotations are designed to help the developer improve the reliability of the code, simplify its modification, and prevent errors when using Java Reflections.

More detailed information about the annotations can be found in the releases description in the project repository. There you can also find instructions on how to connect the library to the project and see a demo application.