Spring beginner here.
I am making two custom annotations @Satisfy and @Verify, meant to be used to track requirements in code.
Each of them have a corresponding @Aspect, which in this snippet just prints a message, but in reality they are used to POST update to gitlab API in order to label requirement issues in gitlab as "satisfied" or "verified".
The @Satisfy aspect is working, it is annotated in the class which implements the required code e.g. the class Addition.
The @Verify aspect is not being called from the JUnit @Test case, as I don't know how to do this. I spent the entire day googling for solutions, and i suspect that aspects might not be able to be used in test cases ?
To sum up:
Satisfy:
package com.example.demo;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME) // The annotation will be available at runtime
@Target(ElementType.METHOD) // This annotation can only be applied to methods
public @interface Satisfy
{
String requirementId() default "No requirementId provided";
}
Verify:
package com.example.demo;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME) // The annotation will be available at runtime
@Target(ElementType.METHOD) // This annotation can only be applied to methods
public @interface Verify
{
String requirementId() default "No requirementId provided";
}
SatisfyAspect:
package com.example.demo;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.lang.reflect.Method;
@Aspect
@Component
public class SatisfyAspect
{
@Around("@annotation(com.example.demo.Satisfy)")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable
{
System.out.println("ENTER SatisfyAspect");
Object proceed = joinPoint.proceed();
return proceed;
}
}
VerifyAspect:
package com.example.demo;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.lang.reflect.Method;
@Aspect
@Component
public class VerifyAspect
{
@Around("@annotation(com.example.demo.Verify)")
public Object verify(ProceedingJoinPoint joinPoint) throws Throwable
{
System.out.println("ENTER VerifyAspect");
Object proceed = joinPoint.proceed();
return proceed;
}
}
Addition class with add requirement: The requirementId is composed of gilab projectid - issue iid
package com.example.demo;
import org.springframework.stereotype.Component;
@Component
public class Addition
{
@Satisfy(requirementId="12345678-1")
int add(int a, int b)
{
return a + b;
}
}
TestApplication:
package com.example.demo;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class DemoApplicationTests
{
@Autowired
private Addition addition;
@Test
void contextLoads()
{
}
@Test
@Verify(requirementId="12345678-1")
void testAdd()
{
assert(addition.add(1, 2) == 3);
}
}
JUnit test classes are not Spring classes, their life cycles are not managed by Spring Boot but by JUnit.
The way to do things like this in JUnit is with extensions and callbacks. In this case I think that a BeforeEachCallback
and/or AfterEachCallback
is probably the solution.
The first thing to do is write the extension. For example:
public class VerifyExtension implements BeforeEachCallback, AfterEachCallback {
@Override
public void beforeEach(ExtensionContext context) {
context.getTestMethod()
.map(method -> method.getAnnotation(Verify.class))
.ifPresent(verify -> System.out.printf("ENTER Verify: %s%n", verify.requirementId()));
}
@Override
public void afterEach(ExtensionContext context) {
context.getTestMethod()
.map(method -> method.getAnnotation(Verify.class))
.ifPresent(verify -> System.out.printf("EXIT Verify: %s%n", verify.requirementId()));
}
}
The next thing to do is automatically activate the extension when a test method is annotated with @Verify
. Otherwise users would have to explicitly annotate the class or method with @ExtendWith(VerifyExtension.class)
. That's easy enough to do: just add that annotation to @Verify
:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@ExtendWith(VerifyExtension.class) // This line is new
public @interface Verify
{
String requirementId() default "No requirementId provided";
}
There is a drawback though. Because the life cycles of Spring Boot and JUnit are not tied together, you can't inject anything in your extension class. Fortunately, SpringExtension
exposes this method which should give you programmatic access to beans:
public static ApplicationContext getApplicationContext(ExtensionContext context)
This of course only works in combination with SpringExtension
, but that's automatically activated when you use @SpringBootTest
.