Gradle + AspectJ + JUnit5
Tags: gradle, java, aspectj, testing
Published: 2024-03-31 17:47:46

## 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:

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 `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`