最近在使用若依(RuoYi-Vue-Plus)框架开发项目时,遇到了一个典型的单元测试问题。当我们在项目中启用了WebSocket功能后,原本运行正常的JUnit单元测试突然开始报错,控制台抛出以下异常:
code复制java.lang.IllegalStateException: jakarta.websocket.server.ServerContainer not available
这个错误发生在Spring Boot应用上下文加载阶段,具体是在初始化serverEndpointExporter这个Bean时。错误信息表明测试环境无法提供WebSocket所需的服务器容器支持。
在Spring Boot项目中,WebSocket功能的实现通常需要以下几个关键组件:
@ServerEndpoint注解的WebSocket端点在常规的Web应用运行时,这些组件都能正常工作。但在单元测试环境下,特别是当测试类仅使用@SpringBootTest注解而没有明确指定Web环境时,Spring会默认使用MOCK环境,这种环境不包含实际的Web容器实现。
当我们在项目中配置WebSocket时,通常会创建一个配置类,如下所示:
java复制@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
这个ServerEndpointExporter会在应用启动时执行以下操作:
@ServerEndpoint注解的类ServerContainer将这些端点注册到Web容器中在生产环境中,应用运行在完整的Web容器(如Tomcat)中,自然提供了ServerContainer的实现。但在测试环境中,情况有所不同:
| 环境类型 | Web容器状态 | ServerContainer可用性 | 适用场景 |
|---|---|---|---|
| MOCK | 模拟的Web环境 | 不可用 | 纯业务逻辑测试 |
| RANDOM_PORT | 真实嵌入式容器 | 可用 | 集成测试 |
| DEFINED_PORT | 真实嵌入式容器 | 可用 | 集成测试 |
| NONE | 无Web环境 | 不可用 | 非Web应用测试 |
让我们分解错误发生的完整链条:
ServerEndpointExporter Bean,调用其afterPropertiesSet()方法ServerContainer实例IllegalStateException最简单的解决方案就是在测试类上明确指定Web环境:
java复制@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class MyTestClass {
// 测试代码
}
这个方案之所以有效,是因为:
RANDOM_PORT会启动一个真实的嵌入式Web容器(通常是Tomcat)ServerContainer实现对于更复杂的测试场景,我们可能需要更精细的控制:
java复制@SpringBootTest(
classes = {MyApp.class, WebSocketTestConfig.class},
webEnvironment = WebEnvironment.RANDOM_PORT,
properties = {
"server.port=0", // 随机端口
"spring.main.allow-bean-definition-overriding=true"
}
)
@ActiveProfiles("test")
public class WebSocketIntegrationTest {
// 测试代码
}
这种配置提供了以下优势:
在某些情况下,我们可能希望测试既能支持WebSocket测试,也能在不相关测试中保持轻量。这时可以使用条件化配置:
java复制@Configuration
@ConditionalOnWebApplication
public class WebSocketTestConfig {
@Bean
@ConditionalOnMissingBean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
然后在测试类中按需激活:
java复制// 需要WebSocket的测试
@SpringBootTest(webEnvironment = RANDOM_PORT)
@Import(WebSocketTestConfig.class)
public class WebSocketRequiredTest {}
// 不需要WebSocket的普通测试
@SpringBootTest
public class RegularTest {}
根据项目实际情况,建议将测试分为三类:
WebSocket测试会显著增加测试执行时间,可以采用以下优化策略:
@DirtiesContext标注会修改上下文的测试类,避免状态污染java复制@SpringBootTest(webEnvironment = RANDOM_PORT)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class SharedContextTests {}
端口冲突问题:
server.port=0或不同的端口号上下文加载失败:
ApplicationContext未能加载WebSocket连接失败:
针对若依框架的特殊性,还需要注意:
java复制@TestPropertySource(properties = {
"ruoyi.shiro.enabled=false"
})
java复制@SpringBootTest(classes = {RuoYiApplication.class, WebSocketConfig.class})
Spring的测试框架在背后做了大量工作:
webEnvironment配置决定是否启动Web容器当指定RANDOM_PORT或DEFINED_PORT时:
ServerContainer实例并注册到Servlet上下文中ServerEndpointExporter通过以下方式获取ServerContainer:java复制ServerContainer container = (ServerContainer) servletContext.getAttribute("jakarta.websocket.server.ServerContainer");
对于需要主动测试WebSocket行为的场景,可以使用以下客户端库:
Java客户端:
java复制@Autowired
private TestRestTemplate restTemplate;
@Test
public void testWebSocket() throws Exception {
int port = this.restTemplate.getRestTemplate().getUriTemplateHandler()
.getRootUri().replace("http://localhost:", "");
WebSocketClient client = new StandardWebSocketClient();
WebSocketSession session = client.doHandshake(
new MyWebSocketHandler(),
"ws://localhost:" + port + "/ws-endpoint"
).get();
}
第三方库:如Tyrus、Autobahn等提供更丰富的测试功能
合理的测试策略应该遵循测试金字塔原则:
WebSocket测试属于集成测试范畴,具有以下特点:
在CI环境中,可以采取以下措施:
我在实际项目中发现,合理组织测试套件可以显著提升开发效率。对于WebSocket相关功能,建议单独建立测试类,并添加@Tag("websocket")注解,便于选择性执行:
java复制@SpringBootTest(webEnvironment = RANDOM_PORT)
@Tag("websocket")
public class WebSocketIntegrationTests {
// 专门的WebSocket测试
}
然后在构建配置中灵活控制:
xml复制<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<groups>websocket</groups>
<!-- 或 -->
<excludedGroups>slow,websocket</excludedGroups>
</configuration>
</plugin>