在Android项目的本地单元测试里,真实对象往往依赖Context、网络层或数据库,直接实例化会导致测试无法在JVM上运行。Mockito作为Java生态最流行的模拟框架,通过运行时生成子类或代理来替代真实实现,让开发者能够控制方法返回值、抛出异常并确认调用行为。理解它的工作机制和正确使用方式,是写出稳定且快速单元测试的关键。

Mockito的底层代理与模拟对象创建原理
Mockito在早期版本使用CGLIB,而现在主流版本基于Byte Buddy和ASM在运行时修改字节码。当我们调用mock(Service.class)时,框架会动态生成一个继承自目标类的子类,并重写所有非final方法。这些方法体内部不再执行原有逻辑,而是将调用转发给Mockito内部的InvocationContainer,从而记录调用信息或返回预设的桩值。正因为如此,final类、final方法以及静态方法在默认情况下无法被模拟,这也是很多初学者在Android测试中遇到问题的根源。
与手动编写假对象(Fake或Stub)相比,Mockito的优势在于不需要为每个依赖维护一套实现代码。假设有一个用户仓储接口,传统做法要写FakeUserRepository并实现十几个方法,而Mockito只需一行声明即可获得具备完整方法签名的模拟实例。此外,Mockito在创建对象时采用惰性策略,未设定行为的方法默认返回null、空集合或基本类型默认值,这降低了测试准备的复杂度,但也要求我们在断言时明确区分“未调用”和“返回默认值”的差别。
在Android Gradle项目中,通常将Mockito依赖放在testImplementation而非androidTestImplementation,因为本地单元测试运行在本地JVM,不需要Instrumentation。若需要模拟Android SDK中的类,可以配合mockito-inline开启内联模拟,从而绕过部分final限制。下面的代码展示了最基本的模拟对象创建方式:
import static org.mockito.Mockito.*;
import org.junit.Test;
public class SampleTest {
static class NetworkClient {
public String fetch() {
return "real";
}
}
@Test
public void testMockCreation() {
NetworkClient client = mock(NetworkClient.class);
// 设定桩值
when(client.fetch()).thenReturn("fake");
System.out.println(client.fetch());
}
}
使用when与verify控制行为并验证交互
when(...).thenReturn(...)是设定模拟对象返回值的核心语法,它分为两个部分:先执行被包装的方法调用以注册期望,再指定返回结果。需要注意的是,when内部的方法调用并不会真正运行原方法,而是被Mockito拦截并记录为一个待匹配的调用模式。如果同一个方法被设定了多个桩,后设定的会覆盖先前的,因此在组织测试代码时应当避免分散定义导致可读性下降。
另一组重要API是verify,用于确认某个方法是否按预期被调用。例如verify(client, times(1)).fetch()表示fetch方法应当恰好被调用一次。然而过度使用verify会让测试与实现细节紧耦合:一旦重构了内部调用顺序,即使功能正确测试也会失败。推荐做法是优先验证被测对象的最终状态或输出,仅在确实需要确认副作用(如发送了日志、触发了回调)时才使用verify。下面的例子演示了两者的配合:
import static org.mockito.Mockito.*;
import org.junit.Test;
public class UserServiceTest {
static class UserApi {
public String load(int id) { return "u" + id; }
}
static class Presenter {
private final UserApi api;
public Presenter(UserApi api) { this.api = api; }
public String show(int id) { return api.load(id); }
}
@Test
public void testPresenter() {
UserApi api = mock(UserApi.class);
when(api.load(1)).thenReturn("mocked");
Presenter p = new Presenter(api);
String result = p.show(1);
// 验证返回值
assert "mocked".equals(result);
// 验证交互
verify(api).load(1);
}
}
在Android开发中,常常结合@Mock注解与MockitoAnnotations.openMocks或MockitoJUnitRunner来减少样板代码。使用运行器后,字段上的@Mock会自动初始化,且测试结束后自动释放,防止内存泄漏。对于Kotlin代码,还需注意尾随lambda和平台的nullable类型,必要时用nullable()匹配器处理空安全校验,否则会出现参数不匹配的异常。
Android场景下常见误区与替代方案对比
第一个常见误区是试图用Mockito模拟Android框架的final类,如Context或SharedPreferences。在标准Mockito中这会抛出CannotMockException,因为相关类被标记为final。解决思路有两种:一是引入mockito-inline开启实验性内联模拟;二是遵循依赖倒置原则,将框架对象包装进自定义接口,仅模拟自写接口。后者虽然增加了抽象层,但让生产代码更清晰且更易测试。
第二个误区是把集成测试与单元测试混为一谈。Mockito适合隔离依赖的单元测试,但如果目标是验证多个组件协作,应当使用Robolectric或真实设备仪表测试。下表列出了不同测试策略的适用边界:
| 测试类型 | 运行环境 | 是否使用Mockito | 典型用途 |
|---|---|---|---|
| 本地单元测试 | JVM | 是,模拟所有Android依赖 | 业务逻辑、数据转换 |
| 仪表测试 | 模拟器或真机 | 少量,配合真实SDK | UI交互、系统权限 |
| Robolectric测试 | JVM加框架影子 | 可选,模拟自定义依赖 | 组件生命周期 |
除了Mockito,Android开发者也会接触Fake实现和Jetpack的TestDispatcher。Fake通过真实但简化的逻辑提供稳定数据,适合长期维护的测试套件;Mockito则更灵活,适合临时切断不确定依赖。实际项目中往往混合使用:用Fake替代数据库,用Mockito验证网络层调用次数。只有认清每种手段的成本,才能在测试覆盖率和执行速度之间找到平衡。
import org.mockito.Mockito.*
import org.junit.Test
class RepoTest {
class Api {
fun get() = "real"
}
@Test
fun mockKt() {
val api = mock(Api::class.java)
`when`(api.get()).thenReturn("fake")
println(api.get())
}
}
最后要强调的是,模拟对象只是手段而非目的。当一份测试需要模拟超过五个协作者时,往往说明被测单元职责过重,应当考虑拆分。保持每个测试只关注一个行为,并利用Mockito的清晰API表达意图,才能让Android单元测试真正成为重构的安全网而非负担。
Android单元测试Mockito模拟对象修改时间:2026-08-23 08:42:54