LogAppenderExtension.java
/**
* Copyright (C) 2022 Christopher J. Stehno
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.github.cjstehno.testthings.junit;
import io.github.cjstehno.testthings.slf4j.AppenderConfig;
import io.github.cjstehno.testthings.slf4j.InMemoryLogAppender;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.*;
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
import static io.github.cjstehno.testthings.util.Reflections.extractValue;
import static org.junit.jupiter.api.extension.ExtensionContext.Namespace.create;
import static org.junit.platform.commons.support.HierarchyTraversalMode.TOP_DOWN;
import static org.junit.platform.commons.support.ReflectionSupport.findFields;
/**
* JUnit 5 extension used to provide and manage the {@link InMemoryLogAppender} for testing. It will setup and tear down
* the logging configuration, based on a default {@link AppenderConfig} field, or one specified by an {@link ApplyLogging}
* annotation on the test method.
* <p>
* If a test method has a parameter of type {@link InMemoryLogAppender} it will be populated with the instance of the
* appender for use in the test (for verification purposes).
*/
@Slf4j
public class LogAppenderExtension implements BeforeEachCallback, AfterEachCallback, ParameterResolver {
private static final String APPENDER_CONFIG_NAME = "APPENDER_CONFIG";
private static final String APPENDER = "appender";
private static final Namespace NAMESPACE = create("test-things", "log-appender");
@Override public void beforeEach(final ExtensionContext context) throws Exception {
val appenderConfig = resolveAppenderConfig(context);
val appender = new InMemoryLogAppender(appenderConfig);
context.getStore(NAMESPACE).put(APPENDER, appender);
appender.attach();
}
@Override public void afterEach(final ExtensionContext context) throws Exception {
((InMemoryLogAppender) context.getStore(NAMESPACE).remove(APPENDER)).detach();
}
@Override
public boolean supportsParameter(final ParameterContext parameterContext, final ExtensionContext extensionContext) throws ParameterResolutionException {
return extensionContext.getRequiredTestMethod().isAnnotationPresent(Test.class)
&& parameterContext.getParameter().getType().equals(InMemoryLogAppender.class);
}
@Override
public Object resolveParameter(final ParameterContext parameterContext, final ExtensionContext extensionContext) throws ParameterResolutionException {
return extensionContext.getStore(NAMESPACE).get(APPENDER);
}
private static AppenderConfig resolveAppenderConfig(final ExtensionContext context) throws Exception {
// use the method annotation value if present
if (context.getRequiredTestMethod().isAnnotationPresent(ApplyLogging.class)) {
val anno = context.getRequiredTestMethod().getAnnotation(ApplyLogging.class);
return extractConfigValue(context, anno.value());
}
// resolve the default configuration
return extractConfigValue(context, APPENDER_CONFIG_NAME);
}
private static AppenderConfig extractConfigValue(final ExtensionContext context, final String fieldName) {
return findFields(
context.getRequiredTestClass(),
f -> AppenderConfig.class.isAssignableFrom(f.getType()) && f.getName().equals(fieldName),
TOP_DOWN
).stream()
.findFirst()
.map(f -> extractValue(context.getRequiredTestInstance(), f, AppenderConfig.class))
.orElseThrow();
}
}