Disable config service client activated with spring.config.import in SpringBootTest
Asked Answered
Z

5

14

I have a bare Spring Boot application

@SpringBootApplication
public class ClientApplication {
    public static void main(String[] args) {
        SpringApplication.run(ClientApplication.class, args);
    }
}

which connects to a Spring Cloud Config Server with the following application.yml

spring:
  application:
    name: client

  config:
    import: configserver:http://localhost:8888

The application works fine when the config server is running and fails as expected when the server is not running.

I now want to write an integration test with @SpringBootTest for the application that does not depend on a running config server, and does not even try to connect to it.

With the config server down, the bare test

@SpringBootTest
class ClientApplicationTests {
    @Test
    void contextLoads() {
    }
}

fails with java.net.ConnectException: Connection refused, which is expected.

When I try to disable the config client with

@SpringBootTest(properties = "spring.cloud.config.enabled=false")

the test does not try to connect to the server but fails with

Failed to load ApplicationContext
java.lang.IllegalStateException: Failed to load ApplicationContext
    at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:132)
    ...
Caused by: java.lang.IllegalStateException: Unable to load config data from 'configserver:http://localhost:8888'
    at org.springframework.boot.context.config.StandardConfigDataLocationResolver.getReferences(StandardConfigDataLocationResolver.java:141)
    ...
Caused by: java.lang.IllegalStateException: File extension is not known to any PropertySourceLoader. If the location is meant to reference a directory, it must end in '/' or File.separator
    at org.springframework.boot.context.config.StandardConfigDataLocationResolver.getReferencesForFile(StandardConfigDataLocationResolver.java:229)

The problem is that the property spring.config.import that has been newly introduced with Spring Boot 2.4 works differently than all other properties. It seems to me that only values can be added to it but never removed.

Is there a way to override spring.config.import in a SpringBootTest? Is there way to suppress only connections to config servers?

Zaxis answered 8/8, 2021 at 16:3 Comment(1)
It's possible to do this with profiles. I used the accepted answer here for inspiration: #69509868Fancher
C
11

Solution for tests only

configserver.import = configserver:https://localhost:8888
spring.config.import = ${configserver.import}

This way you can set the property configserver.import to empty string in your tests:

configserver.import = 

The now empty spring.config.import property will not be considered anymore. This would even work with -D JVM launch parameter overrides in prod environments.

This has to be paired with a spring.cloud.config.enabled=false in test scenarios as spring cloud config checks for at least one spring.config.import value to start with the configserver: prefix and won't let you start if it does not find a matching property.

The error

Caused by: java.lang.IllegalStateException: File extension is not known to any PropertySourceLoader

is caused by by disabling spring cloud config via spring.cloud.config.enabled=false as that also removes its PropertySourceLoader bean that is handling all configserver: prefixed spring.config.import.

Solution for prod environment URL replacements (also includes a fix for tests)

Here is another variant which allows for switching out the config server url in prod environments as multiple spring.config.import definitions do not override each other:

configserver.import = configserver:${CONFIG_SERVER_URL:https://localhost:8888}
spring.config.import = ${configserver.import}
spring.cloud.config.import-check.enabled = false

Why is spring.cloud.config.import-check.enabled necessary? Because spring cloud config requires at least one configserver: prefixed value in spring.config.import but does not resolve property replacements before doing this check. Therefor spring cloud config only sees ${configserver.import} as a string and will prevent your application from starting. spring.cloud.config.import-check.enabled = false prevents the aforementioned check. Furthermore the environment variable CONFIG_SERVER_URL can be used to override the localhost configserver in prod environments while still allowing spring.config.import to be an empty string if configserver.import is set to empty in tests.

/rant: what a nightmare

Coreligionist answered 27/10, 2021 at 13:47 Comment(2)
nightmare is right -- I am upgrading a bunch of applications from Spring Boot 2.3.x all the way to 2.6.x -- and this is driving me crazy -- no wonder most articles on the subject simply point out that you can enable legacy processing -- no one seems to have a handle on how to do it "right"Rathbun
Sadly there is no "right" way, just this - it's very frustrating.Coreligionist
Z
8

After doing some more research, I found the following github issue: https://github.com/spring-cloud/spring-cloud-config/issues/1877

It seems to me that the unsatisfying solution is to never put a spring.config.import in the application.yml if you need to override it in a test.

For the config server in particular, it should only be placed in a profile specific configuration file or a profile specific document of a multi-document application.yml. Then you can activate that profile in production and keep it inactive in tests and development phases.

Zaxis answered 8/8, 2021 at 18:24 Comment(1)
If someone wonders how to configure multi-document (or what it is) see: baeldung.com/spring-yaml#spring-yaml-fileSimar
T
2

If you need config manager everywhere except tests then I think better solution to your problem is to put spring.config.import=configserver:http://localhost:8888 in /scr/main/resources/application.properties instead of .yml file in root dir of your project. Then you simply disable it in tests with spring.cloud.config.enabled=false.

Another solution (in case you don't want to disable it completely is to add optional keyword to that import (spring.config.import=optional:configserver:http://localhost:8888(that way you can keep it in .yml but you will lose ability to disable it in tests, but it will simply ignore fact that server is down.

Throve answered 24/3, 2022 at 18:36 Comment(1)
spring.config.import=optional:configserver:localhost:8888 ... works and simpleSymmetrical
G
0

My solution is to make spring think the location has been loaded using a custom resource loader.

This Class will return an optional resource when it matches an import with the prefix configserver:

package com.roblovelock.spring.cloud.config;

import org.springframework.boot.context.config.*;
import org.springframework.cloud.config.client.ConfigServerConfigDataLocationResolver;
import org.springframework.core.Ordered;

import java.io.IOException;
import java.util.List;

public class NoOpCloudConfig implements ConfigDataLocationResolver<ConfigDataResource>, Ordered {

    @Override
    public boolean isResolvable(ConfigDataLocationResolverContext context, ConfigDataLocation location) {
        return location.hasPrefix(ConfigServerConfigDataLocationResolver.PREFIX);
    }

    @Override
    public List<ConfigDataResource> resolve(ConfigDataLocationResolverContext context, ConfigDataLocation location) throws ConfigDataLocationNotFoundException, ConfigDataResourceNotFoundException {
        return List.of(new NoOpResource());
    }

    @Override
    public List<ConfigDataResource> resolveProfileSpecific(ConfigDataLocationResolverContext context, ConfigDataLocation location, Profiles profiles) throws ConfigDataLocationNotFoundException {
        return List.of(new NoOpResource());
    }

    @Override
    public int getOrder() {
        return -1;
    }

    public static class NoOpResource extends ConfigDataResource{
        private NoOpResource(){
            super(true);
        }
    }

    public static class NoOpResourceLoader implements ConfigDataLoader<NoOpResource> {
        @Override
        public ConfigData load(ConfigDataLoaderContext context, NoOpResource resource) {
            return ConfigData.EMPTY;
        }
    }
}

To work we need to register the Class using spring.factories...

In src/test/resources add a META-INF/spring.factories file with the content

org.springframework.boot.context.config.ConfigDataLocationResolver=com.roblovelock.spring.cloud.config.NoOpCloudConfig
org.springframework.boot.context.config.ConfigDataLoader=com.roblovelock.spring.cloud.config.NoOpCloudConfig.NoOpResourceLoader

Also ensure your test config contains spring.cloud.config.enabled=false.

Ganof answered 26/8, 2022 at 14:28 Comment(0)
D
0

My solution is for cases when ConfigService is discovered from Eureka. Properties are the following:

spring:
  application:
    name: app-name
  profiles:
    active: dev
  cloud:
    config:
      fail-fast: false
      discovery:
        enabled: true
        service-id: app-config-server
      uri: http://localhost:8888

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
    healthcheck:
      enabled: true

This configuration makes the application lookup properties in config server asking for its URL in Eureka. Tests in such application got broken until I found a working set of properties.

I found a working set:

  1. spring.config.import must set exactly in environment SPRING_CONFIG_IMPORT=optional:configserver:http://app-name-in-eureka
  2. specifying spring.config.import = ${SPRING_CONFIG_IMPORT:} does not matter
  3. specifying spring.config.import in test does not matter
  4. spring.cloud.config.fail-fast.enabled=false - this gets the trick
Dravidian answered 28/12, 2022 at 23:36 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.