JUnit与Mock集成:JUnit4/5中的Mock使用

前面的章节我们聊了Mock的基础用法。但说实话,真正在AOSP项目里跑测试,很少孤立地用Mockito。你得把它和JUnit框架捏合到一起。JUnit4和JUnit5虽然都能跑,但集成方式差异不小。

我记得去年帮团队迁移测试框架时,就踩了不少坑。就拿初始化Mock对象来说,JUnit4用@RunWith,JUnit5换成了@ExtendWith。如果不小心混用,测试就跑不起来。

JUnit Mock集成核心流程与对比 JUnit Mock集成 JUnit4 集成方式 @RunWith(MockitoJUnitRunner.class) @Before + MockitoAnnotations.initMocks(this) 局限:Runner单一,无法与其他Runner共存 JUnit5 集成方式 @ExtendWith(MockitoExtension.class) @BeforeEach + MockitoAnnotations.openMocks(this) 优势:可叠加扩展,支持参数化测试 建议优先使用JUnit5,尤其在AOSP新项目中

JUnit4的Mock集成:经典但受限

JUnit4时代,集成Mock主要靠两种方式。我个人习惯用@RunWith,因为它最简洁。

关键点:JUnit4的Runner机制是单例的,一个类只能有一个@RunWith。

看个具体例子:

@RunWith(MockitoJUnitRunner.class)
public class AudioPolicyServiceTest {

    @Mock
    private AudioFlinger audioFlinger;
    
    @Mock
    private AudioPolicyEffects audioPolicyEffects;
    
    @InjectMocks
    private AudioPolicyService service;
    
    @Test
    public void testSetDeviceConnectionState() {
        when(audioFlinger.getSampleRate(anyInt())).thenReturn(48000);
        int result = service.setDeviceConnectionState(...);
        assertEquals(AudioSystem.SUCCESS, result);
    }
}

但问题来了。如果你还想用Spring的@RunWith(SpringRunner.class)做集成测试,那就冲突了。我曾经遇到一个AOSP伙伴,想把Mockito和Robolectric混用,折腾了半天只能用MockitoAnnotations.initMocks(this)手工初始化。

@RunWith(RobolectricTestRunner.class)
public class MediaPlayerTest {

    @Mock
    private MediaPlayerService service;
    
    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);
    }
    
    @Test
    public void testPlay() {
        // 这里可以愉快地使用mock了
    }
}

这种方式灵活,但@Before方法里必须显式调用initMocks。少写了?Mock对象全为null,NPE直接找上门。

注意:JUnit4下用@Before + initMocks时,要确保父类也有初始化逻辑。否则继承层次深了,Mock对象可能漏初始化。

JUnit5的优雅集成:Extension来了

JUnit5最大的改进之一,就是引入了Extension模型。说白了,它把Runner拆成了可组合的扩展点。你再也不用为Runner冲突发愁了。

为什么我推荐JUnit5?你想想看,AOSP里很多测试需要组合多个扩展,比如Mockito + TemporaryFolder + Spring。JUnit4搞不定的事,JUnit5轻松解决。

最常见的写法:

@ExtendWith(MockitoExtension.class)
public class CameraHalTest {

    @Mock
    private CameraDevice cameraDevice;
    
    @Mock
    private SurfaceTexture surfaceTexture;
    
    @InjectMocks
    private Camera3Device device;
    
    @Test
    void testConfigureStreams() {
        when(cameraDevice.createCaptureSession(any())).thenReturn(...);
        // 测试逻辑
    }
}

如果不方便用@ExtendWith,也有手工方案:

class AudioFlingerTest {

    @Mock
    private AudioThread audioThread;
    
    private MockitoExtension extension;
    
    @BeforeEach
    void setup() {
        MockitoAnnotations.openMocks(this);
    }
    
    @AfterEach
    void cleanup() {
        MockitoAnnotations.openMocks(this).close();
    }
}
小技巧:JUnit5的openMocks返回AutoCloseable对象,记得在@AfterEach里close掉。不然可能会残留Mock数据,影响后续测试。我就在这个坑里摔过一次。

实战对比:JUnit4 vs JUnit5

直接看表格对比,一目了然:

维度 JUnit4 JUnit5
初始化方式 @RunWith(MockitoJUnitRunner.class) @ExtendWith(MockitoExtension.class)
手工初始化 MockitoAnnotations.initMocks(this) MockitoAnnotations.openMocks(this)
扩展组合 不支持(单一Runner) 支持(可叠加多个@ExtendWith)
生命周期回调 @Before/@After(固定) @BeforeEach/@AfterEach(灵活)
推荐场景 小型单元测试,无其他框架依赖 AOSP组件测试,混合框架测试

避坑指南:我踩过的三个坑

  • Runner冲突:JUnit4下,一个@RunWith只能有一个。如果你需要Mockito + Spring,只能用@Before去初始化。我个人建议,新项目直接上JUnit5。
  • 生命周期不匹配:我曾经在JUnit5里误用了@Before(JUnit4的注解),结果初始化方法根本没执行。排查了半天。记住:JUnit5用@BeforeEach和@AfterEach。
  • Mock对象泄漏:JUnit5的openMocks不调用close,Mock对象会保留状态。尤其你复用测试类实例时,问题更明显。养成习惯:在@AfterEach里加一行MockitoAnnotations.openMocks(this).close()。
建议:在build.gradle里统一配置Mockito + JUnit5,避免团队成员混用不同版本。gradle依赖示例:
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
testImplementation 'org.mockito:mockito-junit-jupiter:5.4.0'

嗯,到这里你应该能理清JUnit4和JUnit5的Mock集成差异了。说白了,JUnit5更灵活、更强大,尤其是多扩展组合的能力,在AOSP这种复杂项目里特别实用。我在做多媒体组件测试时,同时用MockitoExtension和TemporaryFolderExtension,代码干净很多。

关于代码示例,我建议你复制到IDE里跑一遍。看到测试绿灯亮起,你自然就理解了。下个知识点会深入Mock的Stubbing与验证,但先把集成环境搞稳,才是王道。