Hi I am using following code to print logs using aop in my webflux app,I have trouble writing unit/integration tests ?can we verify log interactions here?Any help would be appreciated
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Loggable {}
@Aspect
@Slf4j
public class LoggerAspect {
@Around("@annotation(Loggable)")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
var result = joinPoint.proceed();
if (result instanceof Mono) {
var monoResult = (Mono) result;
AtomicReference<String> traceId = new AtomicReference<>("");
return monoResult
.doOnSuccess(o -> {
var response = "";
if (Objects.nonNull(o)) {
response = o.toString();
}
log.info("Enter: {}.{}() with argument[s] = {}",
joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName(),
joinPoint.getArgs());
log.info("Exit: {}.{}() had arguments = {}, with result = {}, Execution time = {} ms",
joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName(),
joinPoint.getArgs()[0],
response, (System.currentTimeMillis() - start));
});
}
}
}
Test Failing. Somehow when i debug pointer is not going inside doOnNext Method.And I am not sure how I can assert log interaction in above Logging aspect.In Junit5 I know I can use mockito for each method and return something ,but how i can rerturn in spock.
class LogAspectTest extends Specification {
private static final String MOCK_METHOD_LOG_VALUE = "mockMethodLogValue"
private Logger log = Mock()
private ProceedingJoinPoint mockJoinPoint = Mock()
private static Mono<String> methodReturn = Mono.just(["Data", "Data"])
private LogAspect logAspect = new LogAspect(log)
@Unroll
def 'logAround verify log interaction'() {
given:
mockJoinPoint.proceed() == Mono.just("Hello")
final Method method = TestClass.class.getMethod("mockMethod")
when:
logAspect.logAround(mockJoinPoint)
then:
interaction { mockJoinPointAndMethodSignatureInteractions(method, methodReturnToUse) }
where:
resultType | methodReturnToUse
'Mono' | methodReturn
}
private void mockJoinPointAndMethodSignatureInteractions(Method method, Publisher result) {
1 * mockJoinPoint.proceed() >> result
1 * log.info() >> ""
}
private static class TestClass {
@Loggable
Mono<String> mockMethod() { return Mono.just("data") }
}
}
Is it recommended to write Integration Test for @Loggable annotation since it just logging not sure how can write Integration Test which assert the log statements
Like I said in my comment, you cannot easily mock a private static final
field without using add-on tools like PowerMock or similar. I think that whenever you need something like that, you should rather refactor your code for better testability. Here is an idea which is far from perfect, but I want to give you an idea about how you could unit-test your aspect. As for an integration test, you can also do that, but ask yourself what you want to test: really the aspect or that Spring AOP pointcut matching works correctly?
Anyway, let us assume that your classes under test are:
package de.scrum_master.stackoverflow.q64164101;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Loggable {}
package de.scrum_master.stackoverflow.q64164101;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
import java.util.Objects;
import java.util.function.Consumer;
@Aspect
public class LogAspect {
private static final Logger log = LoggerFactory.getLogger(LogAspect.class.getName());
@Around("@annotation(Loggable)")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
if (result instanceof Mono)
return ((Mono) result).doOnSuccess(getConsumer(joinPoint, start));
return result;
}
public Consumer getConsumer(ProceedingJoinPoint joinPoint, long start) {
return o -> {
String response = "";
if (Objects.nonNull(o))
response = o.toString();
log.info("Enter: {}.{}() with argument[s] = {}",
joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName(),
joinPoint.getArgs());
log.info("Exit: {}.{}() had arguments = {}, with result = {}, Execution time = {} ms",
joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName(),
joinPoint.getArgs()[0],
response, (System.currentTimeMillis() - start));
};
}
}
See how I factored out the lambda into a helper method? It has two effects:
logAround(ProceedingJoinPoint)
advice method more readable.Mono
results (and not called for other result types).The test in its simplest form could look like this:
package de.scrum_master.stackoverflow.q64164101
import org.aspectj.lang.ProceedingJoinPoint
import reactor.core.publisher.Mono
import spock.lang.Specification
class LogAspectTest extends Specification {
LogAspect logAspect = Spy()
ProceedingJoinPoint joinPoint = Mock()
def "aspect target method returns a Mono"() {
given:
joinPoint.proceed() >> Mono.just("Hello")
when:
logAspect.logAround(joinPoint)
then:
1 * logAspect.getConsumer(joinPoint, _)
}
def "aspect target method does not return a Mono"() {
given:
joinPoint.proceed() >> "dummy"
when:
logAspect.logAround(joinPoint)
then:
0 * logAspect.getConsumer(joinPoint, _)
}
}
Please note how I use a Spy
(i.e. a partial mock based on the original object) in order to selectively stub the helper method.
Update: An alternative for more integrative testing would be to configure your logging framework to log into a target which you can control and verify, e.g. log into an in-memory database or into a buffer which you can access.