场景

最近项目使用了Spring Boot 的STOMP 支持, 来完成服务器与浏览器之间的消息通知功能.

STOMP

首先, 简单介绍一下STOMP 协议, Simple(or Streaming) Text Orientated Messaging Protocol 是一种简单的消息文本协议, 其核心理念是简单与可用性.

在脚本语言(如Ruby, Python和Perl) 的编程环境中, 实现完整的消息协议(AMQP 或者JMS)是比较麻烦的, 同时也可能只需要使用部分的消息操作功能. STOMP 在这种环境中, 提供了简单的消息协议实现的能力.

问题描述

在Java 后台程序中, 使用了依赖注入的方式进行消息的发送.

@Autowire
private SimpMessageSendingOperations messageSendingOperations;

private void afterConnectionClosed(){
  ...
  messageSendingOperations.convertAndSend("/topic/interrupt", interrupt);
}

程序是能够正常运行的, 消息也能够成功地发送给浏览器客户端.

但是, 在该类的运行单元测试的时候, 出现以下的异常

Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [org.springframework.messaging.simp.SimpMessageSendingOperations] found for dependency [org.springframework.messaging.simp.SimpMessageSendingOperations]: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {}

问题跟踪

根据异常的信息判断, Spring 在运行测试的时候, 对于SimpMessageSendingOperations类型, 没有找到合适的Bean 来注入.

但是程序是可以运行的, 所以我就debug 了程序的运行, 发现在运行时, 对于SimpMessageSendingOperations 类型, Spring 注入了SimpleMessaggingTemplate 类实例, 而这个类的构造器是需要传入运行期间构造的MessageChannel 类型的.

而在单元测试中, 是无法构造出SimpleMessaggingTemplate 类实例的.

解决方案也比较单纯, 就是在单元测试中, 给SimpMessageSendingOperations 类型注入合适的Bean 即可.

解决方案

方案1

Spring 提供了TestConfiguration 来进行额外Bean 注入. 所以, 第一次的方案是在测试类中, 添加一个内部类来进行Bean 的添加.

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT, classes = {ApplicationConfig.class})
@TestPropertySource("classpath:application.yml")
public class WebSocketServerTest {  
    ...

    @TestConfiguration
    static class Config {
        @Bean
        public static SimpMessageSendingOperations messageSendingOperations() {
            return mock(SimpMessageSendingOperations.class);
        }
    }
    ...
}

该测试类能够通过运行.

但是, 在运行项目的check 时, 发现其它测试类报出了同样的注入失败问题.

跟踪下来, 对于使用SpringBootTest 注解的测试类, Spring 都会根据传入的Config 来扫描所有的组件, 然后把组件所需的Bean 注入, 然后构造出一个ApplicationContext.

所以, 使用该方案的话, 需要在所有的SpringBootTest 注解类中加入内部类来完成Bean 的添加, 这是不太现实的.

方案2

既然内部类的作用范围受限, 那么就讲其提升为顶级类.

@TestConfiguration
public class SpringTestCommonConfig {

    @Bean
    public static SimpMessageSendingOperations messageSendingOperations() {
        return mock(SimpMessageSendingOperations.class);
    }
}

然后, 修改所有的SpringBootTest 注解的测试类的类头注解.

@SpringBootTest(webEnvironment = RANDOM_PORT, classes = {ApplicationConfig.class,SpringTestCommonConfig.class})

这种方式比方案1优雅一点, 但是仍然需要修改所有的SpringBootTest 注解的测试类.

方案3

利用Spring 的组件扫描机制, 将TestConfiguration 类放到扫描到的组件包中.

@ComponentScan(basePackages = {"com.sample"})
public class ApplicationConfig{...} 

package com.sample // 将其置于ComponentScan 配置的包中.
@TestConfiguration
public class SpringTestCommonConfig {

    @Bean
    public static SimpMessageSendingOperations messageSendingOperations() {
        return mock(SimpMessageSendingOperations.class);
    }
}

使用这种方案, Spring 会在构造ApplicationContext 的时候, 扫描到该测试配置类, 然后将Bean 声明加入到容器中. 从而达到了全局测试配置的效果.