When doing integration testing, it is quite common to start external processes. A typical example is testing if your program is sending emails correctly. GreenMail provides an easy way of doing so and the documentation is helpful as well.
While there are plentiful examples out there demonstrating how to do this, most use a fixed port to start Greenmail on. When running tests in parallel, or when the CI server is running tests on different branches simultaneously, this test will fail due to the port already in use.
I use GreenMail as an example only, the same technique could be applied to other services as well.
Typically, this will look similar to:
@RunWith(SpringRunner.class)
@SpringBootTest
public class SomeServiceTest {
@Value("${smtp.port}") (1)
private int port;
@Value("${email.user}")
private String user;
@Value("${email.password}")
private String password;
private GreenMail smtpServer;
@Autowired
private SomeService sut;
@Before
public void setUp() {
smtpServer = new GreenMail(new ServerSetup(port, null, PROTOCOL_SMTP));
smtpServer.setUser(user, password);
smtpServer.start();
}
@After
public void after() {
smtpServer.stop();
}
@Test
public void emailShouldBeSend() {
// prepare
String toAddress = "receiver@test";
String subject = "sending email from test";
String body = "the body of our test email";
// act
sut.sendEmail(toAddress, subject, body);
// expect
Message[] receivedMessages = smtpServer.getReceivedMessages();
Assert.assertEquals("only one email should be send", 1, receivedMessages.length);
// test other aspects of the message ...
}
}
<1> We inject the parameters we need for the GreenMail service via springs value injection.
The code under test would use the same mechanism.
This is quite a lot of code just to set up and tear down the GreenMail service. If we have more than one test class needing the service, we would have to either duplicate the code or introduce inheritance. Both are no good options.
Move GreenMail into a JUnit Rule
Fortunately, we can use JUnit Rules to move all the setup code into another class and include when needed:
@Component
public class SmtpServerRule extends ExternalResource {
... all the initialisation, setup and teardown from above ...
}
This could than be used in the test as follows:
@Autowired
@Rule
public SmtpServerRule smtpServerRule;
Use a dynamically assigned random port
So far, we still use a statically configured port. Spring provides a utility to find an unused port
with SocketUtils.findAvailableTcpPort()
. This left us with the need to inject the value of the free port back into
the spring environment. And this before the spring context is used to start up the beans we want to test.
The @SpringBootTest
annotation provides a mean of adding/changing values to the Spring environment
via the properties attributes, but these are static values by nature.
One way to solve the problem is by providing an implementation of an ApplicationContextInitializer
that uses the SocketUtils
and add the found port under a given name to the environment:
public static class RandomPortInitializer
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
int randomPort = SocketUtils.findAvailableTcpPort();
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(applicationContext,
"smtp.port=" + randomPort);
}
}
This class must be configured as an initializer:
@ContextConfiguration(initializers = { SmtpServerRule.RandomPortInitializer.class})
Full source code can be found in the GitHub repository. The intermediate steps are provided via tags.