io.bootique.junit5.PolymorphicConfigurationChecker Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of bootique-junit5 Show documentation
Show all versions of bootique-junit5 Show documentation
Provides a test framework on top of Bootique.
/*
* Licensed to ObjectStyle LLC under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ObjectStyle LLC licenses
* this file to you 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.bootique.junit5;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
import io.bootique.config.PolymorphicConfiguration;
import io.bootique.config.TypesFactory;
import io.bootique.log.DefaultBootLogger;
import org.junit.jupiter.api.Assertions;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import static java.util.stream.Collectors.toSet;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* @since 2.0
*/
public class PolymorphicConfigurationChecker {
private Class expectedRoot;
private Class extends T> expectedDefault;
private Set> allExpectedTypes;
@SafeVarargs
protected PolymorphicConfigurationChecker(
Class expectedRoot,
Class extends T> expectedDefault,
Class extends T>... otherConfigs) {
this.expectedRoot = Objects.requireNonNull(expectedRoot);
this.expectedDefault = expectedDefault;
Set> allTypes = new HashSet<>();
allTypes.add(Objects.requireNonNull(expectedRoot));
if (expectedDefault != null) {
allTypes.add(expectedDefault);
}
if (otherConfigs != null) {
allTypes.addAll(Arrays.asList(otherConfigs));
}
this.allExpectedTypes = allTypes;
}
@SafeVarargs
public static void test(
Class expectedRoot,
Class extends T> expectedDefault,
Class extends T>... otherConfigs) {
new PolymorphicConfigurationChecker<>(expectedRoot, expectedDefault, otherConfigs).test();
}
@SafeVarargs
public static void testNoDefault(
Class expectedRoot,
Class extends T>... otherConfigs) {
new PolymorphicConfigurationChecker<>(expectedRoot, null, otherConfigs).test();
}
protected void test() {
Set> loaded = loadedFromSpi();
assertEquals(allExpectedTypes, loaded, "Loaded and expected types do not match");
testRoot();
allExpectedTypes.forEach(t -> {
if (!t.equals(expectedRoot)) {
testNonRoot(t);
}
});
}
protected void testRoot() {
// while boundaries are compiler-checked, let's still verify superclass, as generics in Java are easy to bypass
assertTrue(PolymorphicConfiguration.class.isAssignableFrom(expectedRoot), "Invalid root type: " + expectedRoot);
JsonTypeInfo typeInfo = expectedRoot.getAnnotation(JsonTypeInfo.class);
// TODO: test "property" and "use" values of the annotation
assertNotNull(typeInfo,"Root is not annotated with @JsonTypeInfo");
if (expectedDefault != null) {
assertTrue(hasDefault(typeInfo),
"Default type is not specified on root. Expected: " + expectedDefault.getName());
assertEquals(expectedDefault, typeInfo.defaultImpl(),
"Expected and actual default types are not the same");
} else {
assertFalse(hasDefault(typeInfo),
"Expected no default type, but @JsonTypeInfo sets it to " + typeInfo.defaultImpl().getName() + ".");
}
if (isConcrete(expectedRoot)) {
JsonTypeName typeName = expectedRoot.getAnnotation(JsonTypeName.class);
assertNotNull(typeName,"Concrete root configuration type must be annotated with @JsonTypeName: " + expectedRoot.getName());
}
}
protected void testNonRoot(Class extends T> t) {
// while boundaries are compiler-checked, let's still verify superclass, as generics in Java are easy to bypass
assertTrue(expectedRoot.isAssignableFrom(t),
"Invalid type " + t.getName() + ". Must be a subclass of root type " + expectedRoot.getName());
assertTrue(isConcrete(t), "Non-root configuration type must not be abstract: " + t.getName());
// this check would prevent matching subclasses by class, but we discourage that anyways.. (otherwise FQN
// would have to be used in YAML)
JsonTypeName typeName = t.getAnnotation(JsonTypeName.class);
assertNotNull(typeName,"Non-root configuration type must be annotated with @JsonTypeName: " + t.getName());
}
protected Set> loadedFromSpi() {
Collection> types;
try {
types = new TypesFactory<>(
getClass().getClassLoader(),
PolymorphicConfiguration.class,
new DefaultBootLogger(false)).getTypes();
} catch (Exception e) {
Assertions.fail(e.getMessage());
// dead code; still required to compile...
throw new RuntimeException(e);
}
return types.stream()
.filter(p -> expectedRoot.isAssignableFrom(p))
.collect(toSet());
}
protected boolean isConcrete(Class> type) {
int modifiers = type.getModifiers();
return !Modifier.isAbstract(modifiers) && !Modifier.isInterface(modifiers);
}
protected boolean hasDefault(JsonTypeInfo typeInfo) {
return !typeInfo.defaultImpl().equals(JsonTypeInfo.class);
}
}