将外部 属性 传递给 JUnit 的扩展 class

Passing an external property to JUnit's extension class

我的 Spring 引导项目使用 JUnit 5。我想设置一个需要启动本地 SMTP 服务器的集成测试,所以我实现了一个自定义扩展:

public class SmtpServerExtension implements BeforeAllCallback, AfterAllCallback {

    private GreenMail smtpServer;
    private final int port;

    public SmtpServerExtension(int port) {
        this.port = port;
    }

    @Override
    public void beforeAll(ExtensionContext extensionContext) {
        smtpServer = new GreenMail(new ServerSetup(port, null, "smtp")).withConfiguration(GreenMailConfiguration.aConfig().withDisabledAuthentication());
        smtpServer.start();
    }

    @Override
    public void afterAll(ExtensionContext extensionContext) {
        smtpServer.stop();
    }
}

因为我需要配置服务器的端口我在测试中注册扩展 class 像这样:

@SpringBootTest
@AutoConfigureMockMvc
@ExtendWith(SpringExtension.class)
@ActiveProfiles("test")
public class EmailControllerIT {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Value("${spring.mail.port}")
    private int smtpPort;

    @RegisterExtension
    // How can I use the smtpPort annotated with @Value?
    static SmtpServerExtension smtpServerExtension = new SmtpServerExtension(2525);

    private static final String RESOURCE_PATH = "/mail";
    
    @Test
    public void whenValidInput_thenReturns200() throws Exception {
        mockMvc.perform(post(RESOURCE_PATH)
                .contentType(APPLICATION_JSON)
                .content("some content")
        ).andExpect(status().isOk());
    }
}

虽然这基本上是有效的:我如何使用带有 @Value 注释的 smtpPort(从 test 配置文件中读取)?


更新 1

根据您的建议,我创建了一个自定义 TestExecutionListener

public class CustomTestExecutionListener implements TestExecutionListener {

    @Value("${spring.mail.port}")
    private int smtpPort;

    private GreenMail smtpServer;

    @Override
    public void beforeTestClass(TestContext testContext) {
        smtpServer = new GreenMail(new ServerSetup(smtpPort, null, "smtp")).withConfiguration(GreenMailConfiguration.aConfig().withDisabledAuthentication());
        smtpServer.start();
    };

    @Override
    public void afterTestClass(TestContext testContext) {
        smtpServer.stop();
    }
}

监听器是这样注册的:

@TestExecutionListeners(value = CustomTestExecutionListener.class, mergeMode = MERGE_WITH_DEFAULTS)

当 运行 调用侦听器的测试时,但 smtpPort 始终是 0,因此似乎 @Value 注释未被拾取。

我认为你不应该在这里使用扩展,或者一般来说,任何“raw-level”JUnit 东西(比如生命周期方法),因为你将无法访问应用程序上下文他们将无法对 bean 等执行任何自定义逻辑。

相反,看看 Spring's test execution listeners abstraction

通过这种方法,GreenMail 将成为一个由 spring 管理的 bean(可能在一个特殊的配置中,只在测试中加载)但是由于它成为一个 bean,它将能够加载 属性 值并使用 @Value 注释。

在测试执行侦听器中,您将在测试前启动服务器并在测试后停止(或者整个测试 class 如果您需要 - 它具有用于此的“挂钩”)。

另一方面,请确保您 mergeMode = MergeMode.MERGE_WITH_DEFAULTS 作为 @TestExecutionListeners 注释的参数,否则某些默认行为(例如测试中的自动装配、脏上下文(如果有)等)不会工作。

更新 1

关注问题中的更新 1。这不会起作用,因为侦听器本身不是 spring bean,因此您不能在侦听器本身中自动装配或使用 @Value 注释。 您可以尝试遵循 这可能会有帮助,但最初我的意思不同:

  1. 使 GreenMail 本身成为一个 bean:
@Configuration 
// since you're using @SpringBootTest annotation - it will load properties from src/test/reources/application.properties so you can put spring.mail.port=1234 there 
public class MyTestMailConfig {

   @Bean
   public GreenMail greenMail(@Value(${"spring.mail.port"} int port) {
      return new GreenMail(port, ...);
   }
}

现在可以将此配置放在src/test/java/<sub-package-of-main-app>/中,这样在生产中根本不会加载它

  1. 现在测试执行侦听器只能用于 运行 启动/停止 GreenMail 服务器(据我了解你想在测试前启动它并在测试后停止,否则你根本不需要这些听众 :) )
public class CustomTestExecutionListener implements TestExecutionListener {

    @Override
    public void beforeTestClass(TestContext testContext) {
       GreenMail mailServer = 
            testContext.getApplicationContext().getBean(GreenMail.class);
            mailServer.start();
    } 

    @Override
    public void afterTestClass(TestContext testContext) {
       GreenMail mailServer = 
            testContext.getApplicationContext().getBean(GreenMail.class);
            mailServer.stop();
    }
    
}

另一种选择是自动装配 GreenMail bean 并使用 JUnit 的 @BeforeEach@AfterEach 方法,但在这种情况下,您必须在不同的测试中复制此逻辑 classes 需要这种行为。监听器允许重用代码。