Android 单元测试(二)—— 高级 Mock 技术

1. MockedStatic 的深度应用

MockedStatic 高级用法演示,底层原理通过字节码操作替换静态方法实现:

java 复制代码
@RunWith(MockitoJUnitRunner.class)
public class AdvancedMockStaticTest {
    
    private MockedStatic<AndroidSchedulers> mockedAndroidSchedulers;
    private MockedStatic<CustomSDK> mockedCustomSDK;
    private MockedStatic<CustomLog> mockedCustomLog;
    
    @Before
    public void setUp() {
        // 【功能】Mock 多个静态类
        // 【底层原理】Mockito 为每个静态类创建独立的代理
        mockedAndroidSchedulers = mockStatic(AndroidSchedulers.class);
        mockedCustomSDK = mockStatic(CustomSDK.class);
        mockedCustomLog = mockStatic(CustomLog.class);
        
        // 【功能】设置静态方法的复杂行为
        // 【底层原理】支持链式调用和条件返回
        mockedAndroidSchedulers.when(AndroidSchedulers::mainThread)
            .thenReturn(Schedulers.trampoline());
        
        // 【功能】Mock 静态方法的不同调用场景
        mockedCustomSDK.when(() -> CustomSDK.getInstance())
            .thenReturn(mock(CustomSDK.class));
        
        // 【功能】Mock 静态 void 方法
        mockedCustomLog.when(() -> CustomLog.d(anyString(), anyString()))
            .then(invocation -> {
                // 【功能】自定义静态方法行为
                String tag = invocation.getArgument(0);
                String message = invocation.getArgument(1);
                System.out.println("[" + tag + "] " + message);
                return null;
            });
    }
    
    @After
    public void tearDown() {
        // 【功能】清理静态 Mock,避免内存泄漏
        // 【底层原理】释放字节码操作产生的资源
        if (mockedAndroidSchedulers != null) {
            mockedAndroidSchedulers.close();
        }
        if (mockedCustomSDK != null) {
            mockedCustomSDK.close();
        }
        if (mockedCustomLog != null) {
            mockedCustomLog.close();
        }
    }
    
    @Test
    public void testComplexStaticInteraction() {
        // 【功能】测试复杂的静态方法交互
        CustomSDK sdk = CustomSDK.getInstance();
        assertNotNull(sdk);
        
        // 【功能】验证静态方法调用
        mockedCustomSDK.verify(() -> CustomSDK.getInstance());
        
        // 【功能】验证静态方法调用次数
        mockedCustomLog.verify(() -> CustomLog.d(eq("TEST"), anyString()), times(0));
    }
}
  • MockedStatic<AndroidSchedulers>:Mockito 用于 静态方法 Mock 的核心对象。
  • mockStatic(SomeClass.class):是创建 MockedStatic<SomeClass> 对象的工厂方法,返回一个 MockedStatic<SomeClass> 实例,用于后续配置和验证。诉 Mockito:"我要拦截这个类(SomeClass)的所有静态方法调用。没有这一步,静态方法始终是"真调用",无法被替换。
  • 良好的单测习惯:始终在 @After 中清理所有静态 Mock。静态 Mock 是全局的,一旦不释放会污染全局状态。避免静态 Mock 泄漏到其他测试用例,造成莫名其妙的测试失败。

2. ArgumentCaptor 高级用法

捕获 被 Mock 对象方法调用时传入的参数 ,用于断言对象属性或验证业务逻辑。适合 复杂对象、集合、Map 或多次调用 的参数检查。

java 复制代码
public class AdvancedArgumentCaptorTest {
    
    @Mock
    private UserRepository mockUserRepository;
    
    @Mock
    private EmailService mockEmailService;
    
    @Captor
    private ArgumentCaptor<User> userCaptor;
    
    @Captor
    private ArgumentCaptor<List<String>> emailListCaptor;
    
    @Captor
    private ArgumentCaptor<Map<String, Object>> metadataCaptor;
    
    private UserService userService;
    
    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        userService = new UserService(mockUserRepository, mockEmailService);
    }
    
    @Test
    public void testComplexArgumentCapture() {
        // Arrange
        String username = "testuser";
        String email = "test@example.com";
        List<String> roles = Arrays.asList("USER", "ADMIN");
        
        // Act
        userService.createUserWithRoles(username, email, roles);
        
        // Assert - 捕获复杂对象
        verify(mockUserRepository).save(userCaptor.capture());
        User capturedUser = userCaptor.getValue();
        
        // 【功能】验证捕获对象的属性
        assertEquals(username, capturedUser.getUsername());
        assertEquals(email, capturedUser.getEmail());
        assertNotNull(capturedUser.getCreatedAt());
        
        // 【功能】捕获集合参数
        verify(mockEmailService).sendNotificationEmails(emailListCaptor.capture());
        List<String> capturedEmails = emailListCaptor.getValue();
        assertTrue(capturedEmails.contains(email));
        
        // 【功能】捕获 Map 参数
        verify(mockEmailService).logActivity(metadataCaptor.capture());
        Map<String, Object> capturedMetadata = metadataCaptor.getValue();
        assertEquals("USER_CREATION", capturedMetadata.get("action"));
        assertEquals(username, capturedMetadata.get("username"));
    }
    
    @Test
    public void testMultipleArgumentCapture() {
        // 【功能】捕获多次方法调用的参数
        userService.createUser("user1", "user1@example.com");
        userService.createUser("user2", "user2@example.com");
        userService.createUser("user3", "user3@example.com");
        
        // 【功能】验证多次调用
        verify(mockUserRepository, times(3)).save(userCaptor.capture());
        
        // 【功能】获取所有捕获的参数
        List<User> allCapturedUsers = userCaptor.getAllValues();
        assertEquals(3, allCapturedUsers.size());
        
        // 【功能】验证每个捕获的参数
        assertEquals("user1", allCapturedUsers.get(0).getUsername());
        assertEquals("user2", allCapturedUsers.get(1).getUsername());
        assertEquals("user3", allCapturedUsers.get(2).getUsername());
    }
}

特性:

  • 声明与注入

    java 复制代码
    @Captor
    private ArgumentCaptor<User> userCaptor;
    MockitoAnnotations.initMocks(this);
    • @Captor 简化 ArgumentCaptor.forClass(User.class)
    • 避免手动构造,提高可读性。
  • 单次捕获

    java 复制代码
    verify(mockUserRepository).save(userCaptor.capture());
    User capturedUser = userCaptor.getValue();
    assertEquals("testuser", capturedUser.getUsername());
    • 精准断言传入参数对象的属性。
  • 捕获集合/Map

    java 复制代码
    verify(mockEmailService).sendNotificationEmails(emailListCaptor.capture());
    assertTrue(emailListCaptor.getValue().contains("test@example.com"));
    • 检查集合内容、Map 键值,适合多维数据验证。
  • 多次调用捕获

    java 复制代码
    verify(mockUserRepository, times(3)).save(userCaptor.capture());
    List<User> users = userCaptor.getAllValues();
    assertEquals("user1", users.get(0).getUsername());
    • 可断言调用次数与每次参数。

3. 自定义 Answer 和 Matcher

  • Answer :基于入参/状态的动态桩与副作用;保持轻量、可预测。

  • Matcher :表达复杂入参约束的布尔谓词;判空先行、避免副作用。

  • 固定返回→thenReturn;复杂断言→Captor ;复杂匹配→Matcher ;动态逻辑/副作用→Answer

java 复制代码
public class CustomAnswerMatcherTest {
    
    @Mock
    private DatabaseService mockDatabaseService;
    
    @Test
    public void testCustomAnswer() {
        // 【功能】自定义 Answer 实现复杂逻辑
        when(mockDatabaseService.query(anyString())).thenAnswer(new Answer<List<User>>() {
            @Override
            public List<User> answer(InvocationOnMock invocation) throws Throwable {
                // 【功能】根据参数动态返回结果
                String sql = invocation.getArgument(0);
                if (sql.contains("admin")) {
                    return Arrays.asList(new User("admin", "admin@example.com"));
                } else if (sql.contains("user")) {
                    return Arrays.asList(
                        new User("user1", "user1@example.com"),
                        new User("user2", "user2@example.com")
                    );
                }
                return Collections.emptyList();
            }
        });
        
        // 测试不同的查询
        List<User> adminUsers = mockDatabaseService.query("SELECT * FROM users WHERE role = 'admin'");
        assertEquals(1, adminUsers.size());
        assertEquals("admin", adminUsers.get(0).getUsername());
        
        List<User> regularUsers = mockDatabaseService.query("SELECT * FROM users WHERE role = 'user'");
        assertEquals(2, regularUsers.size());
    }
    
    @Test
    public void testCustomMatcher() {
        // 【功能】自定义 Matcher 进行复杂匹配
        when(mockDatabaseService.save(argThat(new ArgumentMatcher<User>() {
            @Override
            public boolean matches(User user) {
                // 【功能】自定义匹配逻辑
                return user != null && 
                       user.getEmail() != null && 
                       user.getEmail().contains("@") &&
                       user.getUsername() != null &&
                       user.getUsername().length() >= 3;
            }
        }))).thenReturn(true);
        
        // 测试匹配
        assertTrue(mockDatabaseService.save(new User("validuser", "valid@example.com")));
        
        // 【功能】使用 Lambda 表达式简化 Matcher
        when(mockDatabaseService.update(argThat(user -> 
            user.getAge() >= 18 && user.getAge() <= 100
        ))).thenReturn(true);
        
        User adultUser = new User("adult", "adult@example.com");
        adultUser.setAge(25);
        assertTrue(mockDatabaseService.update(adultUser));
    }
}

3.1 Answer:用回调定义返回与副作用

  • 场景:返回值取决于入参/状态 、需要副作用(计数、记录、抛异常)、或替代复杂依赖。

  • 写法:

    java 复制代码
    when(mock.query(anyString())).thenAnswer(inv -> {
        String sql = inv.getArgument(0);
        if (sql.contains("admin")) return List.of(new User("admin","a@x.com"));
        if (sql.contains("user"))  return List.of(new User("u1","1@x.com"), new User("u2","2@x.com"));
        return List.of();
    });
    • InvocationOnMock inv 可拿到方法名、参数、调用次序等,便于分支返回
    • 可做副作用 :记录参数、写测试日志、累加器、条件性 throw
    • void 方法 用法:优先 doAnswer(...).when(mock).voidMethod(...)
  • 注意:

    • 逻辑要确定且轻量,避免在 Answer 里写"业务实现"(否则测试变脆)。
    • 需要断言参数细节时,配合 ArgumentCaptor 更清晰。
    • 若只是固定返回值,优先 thenReturn,避免过度使用 Answer。

3.2 Matcher:用断言函数匹配复杂入参

  • 场景:入参是复杂对象/集合/范围;等值匹配不足以表达业务约束。

  • 写法:

    java 复制代码
    when(mock.save(argThat(new ArgumentMatcher<User>() {
        @Override public boolean matches(User u) {
            return u != null
                && u.getUsername()!=null && u.getUsername().length()>=3
                && u.getEmail()!=null && u.getEmail().contains("@");
        }
    }))).thenReturn(true);
    
    // Lambda 简化
    when(mock.update(argThat(u -> u.getAge()>=18 && u.getAge()<=100))).thenReturn(true);
    • 先判空再取字段,避免 NPE。

    • 需要集合/Map时:

      java 复制代码
      when(mock.saveAll(argThat(list -> list!=null && list.size()==3)));
  • 注意:

    • 只做布尔判断,别在 Matcher 里做副作用。
    • 同一调用中 不要混用"原始值"和"匹配器"(否则 InvalidUseOfMatchersException);统一用 eq(...)/any()/argThat(...)
    • 复杂多字段校验 → 首选 Captor 做结构化断言;Matcher 负责"是否通过"。

3.3 常见技巧

  • 异常分支

    java 复制代码
    when(mock.query(argThat(sql -> sql==null || sql.isBlank())))
        .thenAnswer(inv -> { throw new IllegalArgumentException("empty sql"); });
  • 状态驱动

    java 复制代码
    AtomicInteger n = new AtomicInteger();
    when(mock.next()).thenAnswer(inv -> n.getAndIncrement() < 2); // 前两次 true,之后 false
  • 验证阶段也可用 Matcher

    java 复制代码
    verify(mock).save(argThat(u -> u.getUsername().startsWith("user")));
相关推荐
liang_jy6 小时前
Android 单元测试(一)—— 基础
android·面试·单元测试
前端拿破轮7 小时前
从零到一开发一个Chrome插件(二)
前端·面试·github
南北是北北8 小时前
Android TexureView和SurfaceView
前端·面试
Digitally8 小时前
如何将照片从电脑传输到安卓设备
android·电脑
教程分享大师8 小时前
创维LB2002_S905L3A处理器当贝纯净版固件下载_带root权限 白色云电脑机顶盒
android
whatever who cares8 小时前
Android Activity 任务栈详解
android
idward3078 小时前
Android的USB通信 (AOA Android开放配件协议)
android·linux
且随疾风前行.8 小时前
Android Binder 驱动 - Media 服务启动流程
android·microsoft·binder
恋猫de小郭8 小时前
Flutter 真 3D 游戏引擎来了,flame_3d 了解一下
android·前端·flutter