spring-bootjunitjunit5junit5-extension-model

Passing an external property to JUnit's extension class


My Spring Boot project uses JUnit 5. I'd like to setup an integration test which requires a local SMTP server to be started, so I implemented a custom extension:

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

Because I need to configure the server's port I register the extension in the test class like this:

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

While this is basically working: How can I use the smtpPort annotated with @Value (which is read from the test profile)?


Update 1

Following your proposal I created a custom 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();
    }
}

The listener is registered like this:

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

When running the test the listener gets called but smtpPort is always 0, so it seems as if the @Value annotation is not picked up.


Solution

  • I don't think you should work with Extensions here, or in general, any "raw-level" JUnit stuff (like lifecycle methods), because you won't be able to access the application context from them, won't be able to execute any custom logic on beans and so forth.

    Instead, take a look at Spring's test execution listeners abstraction

    With this approach, GreenMail will become a bean managed by spring (probably in a special configuration that will be loaded only in tests) but since it becomes a bean it will be able to load the property values and use @Value annotation.

    In the test execution listener you'll start the server before the test and stop after the test (or the whole test class if you need that - it has "hooks" for that).

    One side note, make sure you mergeMode = MergeMode.MERGE_WITH_DEFAULTS as a parameter to @TestExecutionListeners annotation, otherwise some default behaviour (like autowiring in tests, dirty context if you have it, etc) won't work.

    Update 1

    Following Update 1 in the question. This won't work because the listener itself is not a spring bean, hence you can't autowire or use @Value annotation in the listener itself. You can try to follow this SO thread that might be helpful, however originally I meant something different:

    1. Make a GreenMail a bean by itself:
    @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, ...);
       }
    }
    
    

    Now this configuration can be placed in src/test/java/<sub-package-of-main-app>/ so that in production it won't be loaded at all

    1. Now the test execution listener could be used only for running starting / stopping the GreenMail server (as I understood you want to start it before the test and stop after the test, otherwise you don't need these listeners at all :) )
    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();
        }
        
    }
    

    Another option is autowiring the GreenMail bean and using @BeforeEach and @AfterEach methods of JUnit, but in this case you'll have to duplicate this logic in different Test classes that require this behavour. Listeners allow reusing the code.