Gradle + AspectJ + JUnit5

Rmag Breaking News

Problem Statement

When running JUnit5 tests using Gradle in Java, I wanted to log the arguments, that a method is receiving. One way was to put logger just before the method is executed. However, there are 2 challenges:

If the method is being used 100 times, we have to add the logger in all 100 places.
If the method is in 3rd party library, there are limitations and you can’t really know what that method is actually receiving internally.

In this tutorial, I will use the following example.
My test case is validating that the input string is not null or blank. Here, I am using asserj-core library. So, my test case looks like:

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

@Test
@DisplayName(“Verify string is not null & blank.”)
public void testStringNotNull() {
var input = “Hello world!”;
Assertions.assertThat(input).isNotBlank();
}

Solution

Aspect Oriented Programming(AOP)

We can use the concept of AOP to weave some code at compile time and execute it before/after/around the desired method.

Pre-requisites

I am using below softwares:

Gradle: 8.6
Java: 17.0.10
Groovy: 3.0.17
Kotlin: 1.9.20

We are using aspectj library to weave the code.
This tutorial is specifically for tests residing in /src/test/java. However, with minor changes, you can achieve the same results for the code residing in /src/main/java.

Step 1: Modify build.gradle

Add plugin declaration.

plugins {
// Other plugin declarations
id “io.freefair.aspectj.post-compile-weaving” version “8.6”
}

Add aspectj dependencies

dependencies {
implementation “org.aspectj:aspectjrt:1.9.22”
implementation “org.aspectj:aspectjweaver:1.9.22”

// Other dependencies
}

Add following snippet

compileTestJava {
ajc {
options {
aspectpath.setFrom configurations.aspect
compilerArgs = [“-Xlint:ignore”, “-Xajruntimetarget:1.5”]
}
}
}

Step 2: Add Aspect & advise

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;

@Slf4j
@Aspect
public class LoggingAspect {
@SneakyThrows
@Before(“call(* org.assertj.core.api.Assertions.assertThat(..))”)
public void beforeAssert(final JoinPoint joinPoint) {
log.debug(“Execution before invocation of AbstractCharSequenceAssert.isNotBlank().”);
final var args = joinPoint.getArgs();
var method = MethodSignature.class.cast(joinPoint.getSignature()).getMethod();
final var parameters = method.getParameters();
for (int i = 0; i < parameters.length; i++) {
final Class<?> parameterType = parameters[i].getType();
final var parameterValue = args[i];
log.info(“Param type: {}, value: {}”, parameterType, parameterValue);
}
}
}

This class represents an Aspect, which cuts across multiple objects.

@Before(“call(* org.assertj.core.api.Assertions.assertThat(..))”)
public void beforeAssert(final JoinPoint joinPoint) {
// code
}

This line defines the before advice. The method should return void. The method should be declared public. It takes 1 parameter of type JoinPoint. The value for this annotation is the regular expression of advice declaration.

Here, in this example, we are specifying to execute code before the invocation of Assertions.assertThat(). Any custom logic that you want to perform goes inside the beforeAssert() method. Here, I am extracting the parameters & their values of a method being intercepted, in this case Assertions.assertThat(input). As you can see assertThat() method is taking only 1 parameter of type java.lang.String.

Once you run the code, you should see the following output in the console.

Execution before invocation of AbstractCharSequenceAssert.isNotBlank().
Param type: class java.lang.String, value: Hello world!

Few things to remember

Your aspect needs to be in the same sources root as your tests. i.e. src/test/java

Leave a Reply

Your email address will not be published. Required fields are marked *