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")));
相关推荐
雨白3 小时前
Kotlin 协程的灵魂:结构化并发详解
android·kotlin
我命由我123453 小时前
Android 开发问题:getLeft、getRight、getTop、getBottom 方法返回的值都为 0
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
Modu_MrLiu3 小时前
Android实战进阶 - 用户闲置超时自动退出登录功能详解
android·超时保护·实战进阶·长时间未操作超时保护·闲置超时
Jeled3 小时前
Android 网络层最佳实践:Retrofit + OkHttp 封装与实战
android·okhttp·kotlin·android studio·retrofit
信田君95273 小时前
瑞莎星瑞(Radxa Orion O6) 基于 Android OS 使用 NPU的图片模糊查找APP 开发
android·人工智能·深度学习·神经网络
tangweiguo030519874 小时前
Kotlin 实现 Android 网络状态检测工具类
android·网络·kotlin
怪兽20145 小时前
Redis常见性能问题和解决方案
java·数据库·redis·面试
nvvas5 小时前
Android Studio JAVA开发按钮跳转功能
android·java·android studio
怪兽20145 小时前
Android多进程通信机制
android·面试
ThreeAu.5 小时前
pytest 实战:用例管理、插件技巧、断言详解
python·单元测试·pytest·测试开发工程师