1. 概述

JUnit 是 Java 中最流行的单元测试框架之一。Spring Boot 更是将其作为应用的默认测试依赖。

本文将对比两个 JUnit 运行器:SpringRunnerMockitoJUnitRunner。我们将深入理解它们的用途和核心差异。

2. @RunWith 与 @ExtendWith 对比

在深入主题前,先回顾下如何扩展 JUnit 基础功能或与其他库集成。

JUnit 4 的扩展机制

JUnit 4 允许通过实现自定义 Runner 类来扩展功能,这些类负责运行测试并添加额外特性。使用 @RunWith 注解调用自定义运行器:

@RunWith(CustomRunner.class)
class JUnit4Test {
    // ...
}

JUnit 5 的扩展机制

JUnit 4 已进入维护状态,被 JUnit 5 取代。新版本带来了全新的引擎和重写的 API,并改变了扩展模型概念。不再需要实现自定义 RunnerRule 类,而是使用 Extension API 配合 @ExtendWith 注解:

@ExtendWith(CustomExtensionOne.class)
@ExtendWith(CustomExtensionTwo.class)
class JUnit5Test {
    // ...
}

与旧版运行器模型不同,单个类可以指定多个扩展。大多数旧版运行器也已重写为对应的扩展。

3. Spring 示例应用

为更好理解,我们创建一个简单的 Spring Boot 应用——一个将字符串转换为大写的转换器。

数据提供者实现

@Component
public class DataProvider {
    
    private final List<String> memory = List.of("baeldung", "java", "dummy");
    
    public Stream<String> getValues() {
        return memory.stream();
    }
}

这是一个 Spring 组件,包含硬编码的字符串值,并提供流式访问方法。

服务层实现

@Service
public class StringConverter {
    
    private final DataProvider dataProvider;

    @Autowired
    public StringConverter(DataProvider dataProvider) {
        this.dataProvider = dataProvider;
    }
    
    public List<String> convert() {
        return dataProvider.getValues().map(String::toUpperCase).toList();
    }
}

该服务从 DataProvider 获取数据并应用大写转换。

现在我们用这个应用编写 JUnit 测试,对比 SpringRunnerMockitoJUnitRunner 的差异。

4. MockitoJUnitRunner

Mockito 是一个模拟框架,常与其他测试框架配合使用,用于返回虚拟数据并避免外部依赖。该库提供了 MockitoJUnitRunner——一个专用的 JUnit 4 运行器,用于集成 Mockito 并利用其能力。

基础测试示例

public class StringConverterTest {
    @Mock
    private DataProvider dataProvider;

    @InjectMocks
    private StringConverter stringConverter;
    
    @Test
    public void givenStrings_whenConvert_thenReturnUpperCase() {
        Mockito.when(dataProvider.getValues()).thenReturn(Stream.of("first", "second"));

        val result = stringConverter.convert();

        Assertions.assertThat(result).contains("FIRST", "SECOND");
    }
}

我们模拟了 DataProvider 使其返回两个字符串。但直接运行会失败:

java.lang.NullPointerException: Cannot invoke "DataProvider.getValues()" because "this.dataProvider" is null

常见踩坑原因

@Mock@InjectMocks 注解未被初始化。有两种解决方案:

方案 1:手动初始化

@Before
public void init() {
    MockitoAnnotations.openMocks(this);
}

方案 2:编程式创建

@Before
public void init() {
    dataProvider = Mockito.mock(DataProvider.class);
    stringConverter = new StringConverter(dataProvider);
}

使用 MockitoJUnitRunner 的优雅方案

移除 init() 方法,添加 @RunWith 注解:

@RunWith(MockitoJUnitRunner.class)
public class StringConverterTest {
    // ...
}

测试成功!运行器自动管理了模拟对象,无需手动初始化。

核心功能总结

MockitoJUnitRunner 是 Mockito 框架的专用运行器
✅ 自动初始化 @Mock@Spy@InjectMock 注解
✅ 无需显式调用 MockitoAnnotations.openMocks()
✅ 自动检测未使用的存根,并在每个测试方法后验证模拟用法(类似 Mockito.validateMockitoUsage()

JUnit 5 迁移方案

@ExtendWith(MockitoExtension.class)
public class StringConverterTest {
    // ...
}

MockitoExtensionMockitoJUnitRunner 的功能移植到新的扩展模型。

5. SpringRunner

深入分析测试会发现:尽管使用了 Spring,我们并未启动 Spring 容器。现在修改示例初始化 Spring 上下文。

基础集成测试

@RunWith(SpringRunner.class)
public class StringConverterTest {
    // ...
}

测试成功!模拟对象被正确初始化,同时启动了 Spring 上下文。

关键特性

SpringRunner 不仅支持 Mockito 注解(类似 MockitoJUnitRunner
✅ 同时初始化 Spring 上下文

完整 Spring 集成示例

@ContextConfiguration(classes = StringConverter.class)
@RunWith(SpringRunner.class)
public class StringConverterTest {
    @MockBean
    private DataProvider dataProvider;

    @Autowired
    private StringConverter stringConverter;

    // ...
}

关键改进点

  1. @MockBean 替代 @Mock:既是模拟对象又是 Spring Bean
  2. 通过 @ContextConfiguration 配置 Spring 上下文
  3. 使用 @Autowired 注入 StringConverter

核心功能总结

SpringRunner 是 JUnit 4 的自定义运行器,提供 Spring TestContext 框架功能
✅ 完全支持 MockitoJUnitRunner 的所有能力(Mockito 是 Spring 默认集成框架)
✅ 支持 Spring 特有注解:@MockBean@SpyBean
✅ 通过 Spring 上下文注入模拟对象

别名说明

⚠️ SpringJUnit4ClassRunnerSpringRunner 的别名,两者可互换使用

JUnit 5 迁移方案

@ExtendWith(SpringExtension.class)
public class StringConverterTest {
    // ...
}

SpringExtension 已与 Spring Boot 的多个测试切片注解集成(如 @SpringBootTest)。

6. 结论

本文深入对比了 SpringRunnerMockitoJUnitRunner

核心差异总结

特性 MockitoJUnitRunner SpringRunner
主要用途 纯 Mockito 集成 Spring 上下文 + Mockito 集成
上下文初始化 ❌ 不启动 Spring 容器 ✅ 启动完整 Spring 容器
注解支持 @Mock, @InjectMocks @MockBean, @SpyBean
适用场景 简单单元测试 集成测试/需要 Spring 上下文的测试

选择建议

  • 纯单元测试:使用 MockitoJUnitRunner(JUnit 4)或 MockitoExtension(JUnit 5)
  • 需要 Spring 上下文:使用 SpringRunner(JUnit 4)或 SpringExtension(JUnit 5)
  • Spring Boot 项目:优先使用 @SpringBootTest + @ExtendWith(SpringExtension.class)

完整示例代码可在 GitHub 获取。


原始标题:SpringRunner vs MockitoJUnitRunner

« 上一篇: Java Weekly, 第464期
» 下一篇: 使用Java获取PDF信息