I wrote some test classes with @SpringBootTest for a Spring Boot application using a web application context. The tests should start a Tomcat instance with a random port.
/**
* Simple Spring Boot test with a web environment which should listening on random port.
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class OnOfTheTestClassses {
@LocalServerPort
private int localPort;
@Test
public void localPort() {
// Should output random port but actually outputs the default of the Tomcat container
Assertions.assertNotEquals(8080, localPort, "Tomcat is listening on default port which is not expected");
}
}
The test code also contains a Spring bean with some auxillary methods for the tests.
/**
* Simple bean for test purposes. The bean needs the port Tomcat is listening on.
*/
@Component
public class DemoTestBean {
@Autowired
private WebServerApplicationContext webServerAppCtxt;
// Disabling the annotation so tomcat will listen only on random port
@PostConstruct
public int getPort() {
// In the @PostConstruct phase the property local.server.port isn't set.
// To get a port the bean uses the web server context.
// This causes tomcat listening on two ports: 8080 and a random port.
// Output: Tomcat started on port(s): 8080 (http) <random port>> (http)
return webServerAppCtxt.getWebServer().getPort();
}
}
Everything seems to be alright until I created a new @SpringBootTest class. Some tests beging to fail because the embedded Tomcat could not be started (error message: Address already in use). So I put on my bath cap and dove into the deep abyusse of my code and its dependencies to find out why Tomcat is using an address which is already used.
The first thing I found out was that Tomcat is listening on two ports: 8080 and a random port, see the following log:
com.example.demo.DemoApplicationTests : Starting DemoApplicationTests using Java 11.0.11 on ME1280 with PID 116328 (started by
com.example.demo.DemoApplicationTests : No active profile set, falling back to 1 default profile: "default"
o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 0 (http)
o.apache.catalina.core.StandardService : Starting service [Tomcat]
org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.62]
o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1404 ms
o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) 45719 (http) with context path ''
com.example.demo.DemoApplicationTests : Started DemoApplicationTests in 2.696 seconds (JVM running for 4.612)
I was confused, why is Tomcat listening on its default port 8080.
After some trial and error with the Spring Boot annotations and configurations I found the culprit. The @PostConstruct method causes Tomcat to listening on port 8080.
For me it seems that the injected WebServerApplicationContext doesn't references a listening tomcat instance and would create a new Connector instance with the default port when webServerAppCtxt.getWebServer().getPort() is called in the @PostConstruct method. Why does it behave like this?
A working example can be found here. Simple checkout and build it with maven or your prefered IDE. The example test will fail if Tomcat is listening on the default port 8080.