【问题标题】:Override default Spring-Boot application.properties settings in Junit Test with dynamic value使用动态值覆盖 Junit Test 中的默认 Spring-Boot application.properties 设置
【发布时间】:2015-09-12 12:58:22
【问题描述】:

我想在测试中覆盖 application.properties 中定义的属性,但 @TestPropertySource 只允许提供预定义的值。

我需要在随机端口 N 上启动服务器,然后将此端口传递给 spring-boot 应用程序。端口必须是临时的,以允许同时在同一主机上运行多个测试。

我不是指嵌入式 http 服务器(jetty),而是在测试开始时启动的一些不同的服务器(例如 zookeeper)并且被测试的应用程序必须连接到它。

实现这一目标的最佳方法是什么?

(这里有一个类似的问题,但答案没有提到临时端口的解决方案 - Override default Spring-Boot application.properties settings in Junit Test

【问题讨论】:

    标签: java spring junit spring-boot automated-tests


    【解决方案1】:

    从 Spring Framework 5.2.5 和 Spring Boot 2.2.6 开始,您可以在测试中使用 Dynamic Properties

    @DynamicPropertySource
    static void dynamicProperties(DynamicPropertyRegistry registry) {
        registry.add("property.name", "value");
    }
    

    【讨论】:

      【解决方案2】:

      感谢 Spring Framework 5.2.5 中所做的更改,@ContextConfiguration 和 ApplicationContextInitializer 的使用可以替换为具有相同目的的静态 @DynamicPropertySource 方法。

      @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);
          }
      }
      

      【讨论】:

        【解决方案3】:

        “干净”的解决方案是使用ApplicationContextInitializer

        请参阅this answer 来回答类似问题。

        另请参阅 this github issue 提出类似问题。

        使用经过清理以保护版权所有者的真实示例总结上述帖子(我有一个 REST 端点,它使用 @Autowired DataSource 需要使用动态属性来知道哪个端口在-内存 MySQL 数据库正在使用):

        1. 您的测试必须声明初始化程序(请参阅下面的 @ContextConfiguration 行)。
        // 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. 您的初始化程序需要将动态属性添加到您的环境中。不要忘记为需要运行的任何清理添加关闭挂钩。以下是使用自定义 DatabaseObject 类设置内存数据库的示例。
        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);
            }
        
        }
        

        当我查看日志时,它表明我的两个测试都使用了这个初始化器类,它们使用了同一个对象(initialize 方法只被调用一次,关闭挂钩也是如此)。因此它会启动一个数据库,让它一直运行直到两个测试都完成,然后关闭数据库。

        【讨论】:

          【解决方案4】:

          您可以像这样覆盖@BeforeClass 中端口属性的值:

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

          【讨论】:

          • 有没有办法在测试后删除这些属性?这确实会污染 JVM 环境。
          • 您可以使用 System.clearProperty(key) 重置属性或将其恢复为 @AfterClass 中的先前值
          • 可悲的是,这种 hack 并不总是有效。如果在多个测试类中完成,环境和/或竞争条件会导致失败。我在本地工作得很好,但是在我的 CircleCI 工作中,只有第一个执行的测试类可以工作,下一个测试类仍然会看到第一个测试类的(不再有效)值,尽管已将属性设置为@BeforeClass 方法中的一些新东西。但只要你只在一个班级里做这件事,这个 hack 似乎就可以正常工作。
          猜你喜欢
          • 2015-06-22
          • 1970-01-01
          • 2020-05-03
          • 2014-12-03
          • 2020-03-04
          • 2015-06-30
          • 2017-01-29
          • 2016-06-06
          • 1970-01-01
          相关资源
          最近更新 更多