22、Content Provider 测试:单元测试、MockContentProvider、测试用例编写
说实话,很多开发者对 Content Provider 的测试都不太重视。觉得这东西不就是封装了一层数据库操作嘛,跑个真机测一下不就行了?
我以前也这么想,直到有一次线上出了个事故——某个三方应用通过我们的 Provider 查询数据时,因为 URI 匹配顺序写错了,直接返回了空结果。用户数据同步失败,反馈炸了。从那以后,我老老实实把 Provider 测试补上了。
今天我们就聊聊,怎么给 Content Provider 写靠谱的测试。
为什么需要单独测试 Provider?
你想想看,Provider 是跨进程通信的桥梁。它不光要处理数据,还要处理 URI 匹配、权限校验、参数解析。这些逻辑如果只靠 UI 测试去覆盖,效率太低了。
单元测试的好处很明显:
- 跑得快——几秒钟就能验证一次
- 不依赖真机——本地 JVM 就能跑
- 能覆盖边界情况——比如传个 null 的 URI 会怎样
核心原则:Provider 测试要验证的是「数据访问协议」,而不是数据库本身。数据库的 CRUD 应该由 DAO 层的测试去覆盖。
测试环境搭建
我习惯用 Robolectric + Mockito 这套组合。Robolectric 能模拟 Android 框架,Mockito 用来 mock 依赖。
在 build.gradle 里加上:
testImplementation 'org.robolectric:robolectric:4.10'
testImplementation 'org.mockito:mockito-core:5.3.0'
testImplementation 'androidx.test:core:1.5.0'
嗯,这里要注意:如果你用的是 AndroidX,测试依赖的版本最好和 compileSdk 对齐,不然容易出兼容性问题。
MockContentProvider 的使用
Android 官方提供了 MockContentProvider 这个类,但说实话,它太基础了。我更推荐自己写一个轻量的 mock 实现,或者直接用 ProviderTestCase2。
来看一个典型的测试结构:
@RunWith(RobolectricTestRunner.class)
@Config(sdk = 28)
public class MyProviderTest {
private MyProvider provider;
private Context context;
@Before
public void setUp() {
context = ApplicationProvider.getApplicationContext();
provider = new MyProvider();
provider.attachInfo(context, null);
}
@Test
public void testQuery_returnsCorrectData() {
Uri uri = Uri.parse("content://com.example.app/items");
Cursor cursor = provider.query(uri, null, null, null, null);
assertNotNull(cursor);
assertTrue(cursor.getCount() > 0);
}
}
这里有个坑——attachInfo 必须在测试里手动调用,否则 Provider 的 onCreate 不会执行。我刚开始写测试时忘了这步,debug 了半天才发现。
测试用例编写策略
我个人习惯把测试用例分成三类:
| 类型 | 测试内容 | 示例 |
|---|---|---|
| 正常路径 | 标准 CRUD 操作 | 插入一条数据,查询能返回 |
| 边界条件 | 空参数、非法 URI | 传 null 的 selection 参数 |
| 异常场景 | 权限不足、数据不存在 | 未授权时调用 delete |
举个例子,测试 URI 匹配:
@Test(expected = IllegalArgumentException.class)
public void testQuery_withInvalidUri_throwsException() {
Uri badUri = Uri.parse("content://com.example.app/wrong_path");
provider.query(badUri, null, null, null, null);
}
为什么会这样?因为 Provider 的 UriMatcher 匹配不到路径时,默认会抛异常。这个行为一定要测到,否则线上可能出现诡异的 500 错误。
Mock 数据库依赖
如果你的 Provider 依赖 Room 或者 SQLiteOpenHelper,我建议把数据库层也 mock 掉。这样测试只关注 Provider 的逻辑,不关心数据库实现。
@Mock
private MyDatabaseHelper dbHelper;
@Mock
private SQLiteDatabase db;
@Before
public void setUp() {
MockitoAnnotations.openMocks(this);
when(dbHelper.getWritableDatabase()).thenReturn(db);
provider = new MyProvider(dbHelper);
}
说白了,Provider 就是个「翻译官」——把 URI 翻译成 SQL 操作。我们测试的就是这个翻译过程对不对。
避坑指南
我曾经踩过的坑:
- 测试里用了
ApplicationProvider,但没加@Config(sdk = 28),结果 Robolectric 默认用最低 API,很多方法找不到。 - Cursor 的
getCount()在 mock 实现里返回 0,但实际 Provider 返回了数据——因为忘了调用cursor.moveToFirst()。 - 多线程场景下,Provider 的
getType()方法被多次调用,导致性能测试失真。
知识体系总览
下面这张图是我自己总结的 Provider 测试核心流程,你可以照着这个思路来组织测试代码:
小技巧:测试 Provider 时,尽量用 ShadowContentResolver 来模拟跨进程调用。Robolectric 默认支持这个 shadow,你不需要额外配置。
总结
Provider 测试其实不难,难的是想清楚「测什么」。我个人建议:
- 每个 URI 路径至少有一个正常查询用例
- 每个写操作(insert/update/delete)都要测返回值
- 权限校验单独写一个测试类
记住,测试不是为了覆盖率数字好看,而是为了让你晚上能睡个安稳觉。