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 测试核心流程,你可以照着这个思路来组织测试代码:

Content Provider 测试核心流程 1. 测试环境搭建 2. Provider 实例化 3a. 正常 CRUD 测试 3b. 边界条件测试 3c. 异常场景测试 4. 断言与验证 测试通过 ✓

小技巧:测试 Provider 时,尽量用 ShadowContentResolver 来模拟跨进程调用。Robolectric 默认支持这个 shadow,你不需要额外配置。

总结

Provider 测试其实不难,难的是想清楚「测什么」。我个人建议:

  • 每个 URI 路径至少有一个正常查询用例
  • 每个写操作(insert/update/delete)都要测返回值
  • 权限校验单独写一个测试类

记住,测试不是为了覆盖率数字好看,而是为了让你晚上能睡个安稳觉。


公众号:蓝海资料掘金营,微信deep3321