@Test
    void testAbstractConfigPresence() throws IOException {

        final AppConfig appConfig = new AppConfig();

        final AppConfigModule appConfigModule = new AppConfigModule(new ConfigHolder() {
            @Override
            public AppConfig getBootStrapConfig() {
                return appConfig;
            }

            @Override
            public Path getConfigFile() {
                return Paths.get("NOT USED");
            }
        });
        final Injector injector = Guice.createInjector(appConfigModule);

        final Predicate<String> packageNameFilter = name ->
                name.startsWith(STROOM_PACKAGE_PREFIX) && !name.contains("shaded");

        final Predicate<Class<?>> classFilter = clazz ->
                clazz.getSimpleName().endsWith("Config")
                        && !clazz.equals(AbstractConfig.class)
                        && !clazz.equals(AppConfig.class)
                        && !Modifier.isPrivate(clazz.getModifiers()) // ignore local sub classes
                        && IsStroomConfig.class.isAssignableFrom(clazz);

        LOGGER.info("Finding all stroom config classes");

        // Find all config classes
        final ClassLoader classLoader = getClass().getClassLoader();
        final Set<Class<?>> stroomConfigClasses = ClassPath.from(classLoader)
                .getAllClasses()
                .stream()
                .filter(classInfo -> packageNameFilter.test(classInfo.getPackageName()))
                .map(ClassPath.ClassInfo::load)
                .filter(classFilter)
                .filter(clazz -> {
                    boolean isAbstract = Modifier.isAbstract(clazz.getModifiers());
                    if (isAbstract) {
                        LOGGER.info("Ignoring abstract class {}", clazz.getName());
                    }
                    return !isAbstract;
                }) // Ignore abstract classes, e.g. UriConfig
                .peek(clazz -> {
                    LOGGER.debug(clazz.getSimpleName());
                })
                .collect(Collectors.toSet());

        // All config classes should extend AbstractConfig and IsStroomConfig
        assertThat(stroomConfigClasses.stream()
                .filter(clazz ->
                        !AbstractConfig.class.isAssignableFrom(clazz)
                                || !IsStroomConfig.class.isAssignableFrom(clazz))
                .collect(Collectors.toList()))
                .isEmpty();

        LOGGER.info("Finding all classes in object tree");

        final Map<Class<?>, Integer> appConfigTreeClassToIdMap = new HashMap<>();

        // Find all stroom. classes in the AppConfig tree, i.e. config POJOs
        final Set<Class<?>> appConfigTreeClasses = new HashSet<>();
        PropertyUtil.walkObjectTree(
                appConfig,
                prop -> packageNameFilter.test(prop.getValueClass().getPackageName()),
                prop -> {
                    // make sure we have a getter and a setter
                    assertThat(prop.getGetter())
                            .as(LogUtil.message("{} {}",
                                    prop.getParentObject().getClass().getSimpleName(), prop.getGetter().getName()))
                            .isNotNull();

                    Class<?> valueClass = prop.getValueClass();
                    if (classFilter.test(valueClass)) {
                        appConfigTreeClasses.add(prop.getValueClass());
                        AbstractConfig propValue = (AbstractConfig) prop.getValueFromConfigObject();
                        // Keep a record of the instance ID of the instance in the tree
                        appConfigTreeClassToIdMap.put(valueClass, System.identityHashCode(propValue));
                    }
                });

        // Make sure all config classes extend AbstractConfig and all AbstractConfig classes are in
        // the AppConfig tree. If there is a mismatch then it may be due to the getter/setter not
        // being public in the config class, else the config class may not be a property in the
        // AppConfig object tree
        Set<Class<?>> remaining = new HashSet<>(appConfigTreeClasses);
        remaining.removeAll(stroomConfigClasses);
        assertThat(remaining)
                .describedAs("Class(es) in the config object tree but that don't implement IsStroomConfig")
                .isEmpty();

        remaining = new HashSet<>(stroomConfigClasses);
        remaining.removeAll(appConfigTreeClasses);
        assertThat(remaining)
                .describedAs("Class(es) that implement IsStroomConfig but aren't in the config object tree")
                .isEmpty();
    }
IMPORTANT: This test must be run from stroom-app so it can see all the other modules
stroom-app/src/test/java/stroom/config/TestAppConfigModule.java