Override default Spring-Boot application.properties settings in Junit Test with dynamic value
Asked Answered
D

4

8

I want to override properties defined in application.properties in tests, but @TestPropertySource only allows to provide predefined values.

What I need is to start a server on a random port N, then pass this port to spring-boot application. The port has to be ephemeral to allow running multiple tests on the same host at the same time.

I don't mean the embedded http server (jetty), but some different server that is started at the beginning of the test (e.g. zookeeper) and the application being tested has to connect to it.

What's the best way to achieve this?

Here's a similar question:

But the answers do not mention a solution for ephemeral ports

Durman answered 25/6, 2015 at 18:51 Comment(0)
E
7

As of Spring Framework 5.2.5 and Spring Boot 2.2.6 you can use Dynamic Properties in tests:

@DynamicPropertySource
static void dynamicProperties(DynamicPropertyRegistry registry) {
    registry.add("property.name", "value");
}
Enharmonic answered 20/4, 2020 at 11:46 Comment(5)
Does this annotation have anything to do with test containers? i.e Does the test class needs to have @TestContainers annotations? When I just add the above method with the annotation it doesn't have any effect.Rowdyism
maybe you should create another question related to your problem. This annotation can be used with testcontainers but testcontainers are not required.Enharmonic
Ok, then I tried this and it doesn't work. I have the SpringBootTest annotation along with ActiveProfiles("integration") but still doesn't work. Any idea what could be going wrong?Rowdyism
Let's move to another question, maybe post a link here?Enharmonic
I have added the problem statement here. I was trying to override the properties in a separate properties file through active profiles but this was one another solution that I was trying. Comment if see it workingRowdyism
P
5

Thanks to the changes made in Spring Framework 5.2.5, the use of @ContextConfiguration and the ApplicationContextInitializer can be replaced with a static @DynamicPropertySource method that serves the same purpose.

@SpringBootTest
@Testcontainers
class SomeSprintTest {

    @Container
    static LocalStackContainer localStack = 
        new LocalStackContainer().withServices(LocalStackContainer.Service.S3);

    @DynamicPropertySource
    static void initialize(DynamicPropertyRegistry registry) {
        AwsClientBuilder.EndpointConfiguration endpointConfiguration = 
            localStack.getEndpointConfiguration(LocalStackContainer.Service.S3);

        registry.add("cloud.aws.s3.default-endpoint", endpointConfiguration::getServiceEndpoint);
    }
}
Parishioner answered 31/3, 2020 at 0:59 Comment(0)
S
3

You could override the value of the port property in the @BeforeClass like this:

@BeforeClass
public static void beforeClass() {
    System.setProperty("zookeeper.port", getRandomPort());
}
Scamander answered 13/5, 2016 at 13:30 Comment(3)
Is there a way to remove these properties after the tests? This does somehow pollute the JVM environment.Mummify
You could reset the property using System.clearProperty(key) or revert it to its previous value in the @AfterClassScamander
Sadly, this hack does not always work. There are environment and/or race conditions that will cause this to fail if done in more than one test class. I had this working fine locally, but on my CircleCI job, only the first executed test class would work, the next test class would still see the (no-longer-valid) value from the first test class, despite having set the property to something new in the @BeforeClass method. But as long as you're only doing it in a single class, this hack seems to work fine.Undersecretary
U
2

The "clean" solution is to use an ApplicationContextInitializer.

See this answer to a similar question.

See also this github issue asking a similar question.

To summarize the above mentioned posts using a real-world example that's been sanitized to protect copyright holders (I have a REST endpoint which uses an @Autowired DataSource which needs to use the dynamic properties to know which port the in-memory MySQL database is using):

  1. Your test must declare the initializer (see the @ContextConfiguration line below).
// standard spring-boot test stuff
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("local")
@ContextConfiguration(
        classes = Application.class,
        // declare the initializer to use
        initializers = SpringTestDatabaseInitializer.class)
// use random management port as well so we don't conflict with other running tests
@TestPropertySource(properties = {"management.port=0"})
public class SomeSprintTest {
    @LocalServerPort
    private int randomLocalPort;

    @Value("${local.management.port}")
    private int randomManagementPort;

    @Test
    public void testThatDoesSomethingUseful() {
        // now ping your service that talks to the dynamic resource
    }
}
  1. Your initializer needs to add the dynamic properties to your environment. Don't forget to add a shutdown hook for any cleanup that needs to run. Following is an example that sets up an in-memory database using a custom DatabaseObject class.
public class SpringTestDatabaseInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

    private static final int INITIAL_PORT = 0; // bind to an ephemeral port
    private static final String DB_USERNAME = "username";
    private static final String DB_PASSWORD = "password-to-use";
    private static final String DB_SCHEMA_NAME = "default-schema";

    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        DatabaseObject databaseObject = new InMemoryDatabaseObject(INITIAL_PORT, DB_USERNAME, DB_PASSWORD, DB_SCHEMA_NAME);
        registerShutdownHook(databaseObject);
        int databasePort = startDatabase(databaseObject);
        addDatabasePropertiesToEnvironment(applicationContext, databasePort);
    }

    private static void addDatabasePropertiesToEnvironment(ConfigurableApplicationContext applicationContext, int databasePort) {
        String url = String.format("jdbc:mysql://localhost:%s/%s", databasePort, DB_SCHEMA_NAME);
        System.out.println("Adding db props to environment for url: " + url);
        TestPropertySourceUtils.addInlinedPropertiesToEnvironment(
                applicationContext,
                "db.port=" + databasePort,
                "db.schema=" + DB_SCHEMA_NAME,
                "db.url=" + url,
                "db.username=" + DB_USERNAME,
                "db.password=" + DB_PASSWORD);
    }

    private static int startDatabase(DatabaseObject database) {
        try {
            database.start();
            return database.getBoundPort();
        } catch (Exception e) {
            throw new IllegalStateException("Failed to start database", e);
        }
    }

    private static void registerShutdownHook(DatabaseObject databaseObject) {
        Runnable shutdownTask = () -> {
            try {
                int boundPort = databaseObject.getBoundPort();
                System.out.println("Shutting down database at port: " + boundPort);
                databaseObject.stop();
            } catch (Exception e) {
                // nothing to do here
            }
        };

        Thread shutdownThread = new Thread(shutdownTask, "Database Shutdown Thread");
        Runtime.getRuntime().addShutdownHook(shutdownThread);
    }

}

When I look at the logs, it shows that for both of my tests that use this initializer class, they use the same object (the initialize method only gets called once, as does the shutdown hook). So it starts up a database, and leaves it running until both tests finish, then shuts the database down.

Undersecretary answered 21/3, 2019 at 0:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.