Spring 单元测试:使用@SpyBean 模拟 void 方法:为该 bean 获取 NullPointerException,除非我改用 @Mock

Spring Unit Testing: Using @SpyBean to mock a void method: getting NullPointerException for that bean unless I use @Mock instead

我正在尝试让@SpyBean 或@MockBean 在这个测试中工作,因为它们在我的所有其他测试中工作。这里唯一的区别是这个测试使用了一个活动配置文件,因为它模拟了一些 AWS 库

如果我使用的是 SpyBean,我就不必实例化 class 它,我已经看到无数与我下面类似的示例,它们似乎工作正常。

我正在测试的单元:

@Component
public class SqsAdapter {

    @Autowired
    QueueMessagingTemplate queueMessagingTemplate;

    @Autowired
    MyProcessor processor;

    @SqsListener("${queue.name}")
    public void onEvent(String message) {
        if (!StringUtils.isEmpty(message)) {
            processor.process(message);
        } else { 
            log.error("Empty or null message received from queue");
        }
    }
}

和一个简单的单元测试,期望调用 process(...):

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
@ActiveProfiles("local")
public class SqsAdapterTest {

    @Autowired
    AmazonSQSAsync amazonSQS;

    @SpyBean
    MyProcessor processor;

    @InjectMocks
    SqsAdapter adapter;

    @Test
    public void testOnEvent() {
        doNothing().when(processor).process(any()); // <---- process() is void
        String event = "royale with cheese";
        adapter.onEvent(event);
        verify(processor, times(1)).process(event);
    }
}

当我在单元测试中调试时,我在 processor 上得到一个 java.lang.NullPointerException ,就好像它从未被初始化过一样:

我也试过将它切换到@MockBean,结果相同

此外,我还尝试剥离对@SpringBootTest 和@ActiveProfiles 的测试...并删除了 AmazonSQSAsync 依赖项,因为我没有模拟 Listener 本身,它仍然会抛出相同的 NPE

我在这里错过了什么?

我猜 Spring 引导测试不会在此测试配置中加载 MyProcessor bean。

通常,如果加载了 bean,则 @SpyBean 注释可以指示 spring 包装原始 bean,以便您获得间谍。但是,您必须提供 spring 引导测试配置,使其能够找到原始的 MyProcessor bean。

如果你有配置class,输入breakpoint/log:


@Configuration
public class MyConfiguration {

    @Bean
    public MyProcessor myProcessor(...) {
       return new MyProcessor(); // <-- put breakpoint here and see whether its called at all
    }
}

或者,如果您有类似的东西:

@Component
public class MyProcessor {
    .... <-- put a breakpoint in constructor (create a default one if you don't have any)
}

如果我的假设是正确的,现在您应该看到根本没有调用此 class,并且您了解 为什么 设置不起作用。

现在,用 @SpringBootTest

代替 @SpringBootTest(classes = Application.class)

这试图通过递归扫描配置和所有内容来模仿 spring 启动应用程序启动过程(这是一个广泛的主题,实际上您应该阅读 spring 启动测试的文档)。

相反,你所做的是对 spring 说的:"I just want to work with predefined configuration class Application and that's it, don't scan anything, don't resolve configurations, don't resolve beans, I know what I'm doing"。

如果您遵循 spring 引导提供的包约定,在这一步之后,您应该会看到加载了原始 MyProcessor bean 并创建了 spy

我想我已经回答了:似乎是因为通过将@InjectMocks 注释与@SpyBean 混合来混合测试框架...因为我试图不使用 Mockito 模拟,这是一个 Mockito 注释,我认为它搞砸了测试

工作单元测试如下所示:

@RunWith(SpringRunner.class)
@SpringBootTest
@ActiveProfiles("local")
public class SqsAdapterTest {

    @Autowired
    AmazonSQSAsync amazonSQS;

    @SpyBean
    SeamEventProcessor seamEventProcessor;

    @Autowired
    SqsAdapter adapter;

    @Test
    public void testOnEvent() {
        doNothing().when(seamEventProcessor).process(any());
        String event = "royale with cheese";
        adapter.onEvent(event);
        verify(seamEventProcessor, times(1)).process(event);
    }
}