Override default Spring-Boot application.propertie

2019-04-08 04:41发布

问题:

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 answers do not mention a solution for ephemeral ports - Override default Spring-Boot application.properties settings in Junit Test)

回答1:

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

@BeforeClass
public static void beforeClass() {
    System.setProperty("zookeeper.port", getRandomPort());
}


回答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.