Gradle + AspectJ + JUnit5

vishal_bhavsar

Vishal Bhavsar

Posted on March 31, 2024

Gradle + AspectJ + JUnit5

Problem Statement

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

  1. If the method is being used 100 times, we have to add the logger in all 100 places.
  2. 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 assertj-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();
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

Add aspectj dependencies

dependencies {
    implementation "org.aspectj:aspectjrt:1.9.22"
    implementation "org.aspectj:aspectjweaver:1.9.22"

    // Other dependencies
}
Enter fullscreen mode Exit fullscreen mode

Add following snippet

compileTestJava {
    ajc {
        options {
            aspectpath.setFrom configurations.aspect
            compilerArgs = ["-Xlint:ignore", "-Xajruntimetarget:1.5"]
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

Few things to remember

  • Your aspect needs to be in the same sources root as your tests. i.e. src/test/java
💖 💪 🙅 🚩
vishal_bhavsar
Vishal Bhavsar

Posted on March 31, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Gradle + AspectJ + JUnit5
gradle Gradle + AspectJ + JUnit5

March 31, 2024