30、联系人系统测试:单元测试、UI测试、性能测试与兼容性测试

说实话,写代码只是开发的一半。另一半,是证明你的代码真的能跑。

我见过太多项目,功能开发得飞起,一到测试阶段就崩。尤其是联系人这种系统级应用,数据敏感、交互频繁、还要适配各种ROM。你不把测试做扎实,上线就是给自己挖坑。

这一章,我把联系人系统的测试拆成四个维度来讲:单元测试(重点讲ContentProvider)、UI测试(Espresso)、性能测试(Profiler)、兼容性测试(不同厂商ROM)。每个维度我都会结合自己的踩坑经历,给你一些实用的建议。

核心观点:测试不是可有可无的环节,而是保障代码质量的最后一道防线。尤其是ContentProvider的测试,很多人忽略,但恰恰是数据层最容易出问题的地方。

联系人系统测试知识体系 联系人系统测试 单元测试 ContentProvider测试 UI测试 Espresso框架 性能测试 Profiler工具 兼容性测试 不同厂商ROM ProviderTestCase2 CRUD验证 Uri匹配测试 onView匹配 RecyclerView操作 Intent验证 CPU分析 内存快照 网络流量 华为/小米/OV 权限差异 ContentProvider差异

1. 单元测试:ContentProvider 测试

单元测试是测试金字塔的底座。对于联系人系统,ContentProvider 是数据层的核心,必须单独测。

我个人习惯用 ProviderTestCase2 来做 ContentProvider 的测试。它会在内存中创建一个模拟的 ContentProvider 环境,不依赖真实设备。

public class ContactProviderTest extends ProviderTestCase2<ContactProvider> {
    
    private MockContentResolver resolver;
    
    public ContactProviderTest() {
        super(ContactProvider.class, "com.example.contacts.provider");
    }
    
    @Override
    protected void setUp() throws Exception {
        super.setUp();
        resolver = getMockContentResolver();
    }
    
    // 测试插入联系人
    public void testInsertContact() {
        ContentValues values = new ContentValues();
        values.put(ContactsContract.Contacts.DISPLAY_NAME, "张三");
        values.put(ContactsContract.Contacts.PHONE_NUMBER, "13800138000");
        
        Uri uri = resolver.insert(ContactsContract.Contacts.CONTENT_URI, values);
        assertNotNull("插入失败,uri为空", uri);
        
        // 验证插入后的数据
        Cursor cursor = resolver.query(uri, null, null, null, null);
        assertTrue("查询结果为空", cursor.moveToFirst());
        assertEquals("姓名不匹配", "张三", 
            cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME)));
        cursor.close();
    }
    
    // 测试删除联系人
    public void testDeleteContact() {
        // 先插入一条数据
        ContentValues values = new ContentValues();
        values.put(ContactsContract.Contacts.DISPLAY_NAME, "李四");
        Uri uri = resolver.insert(ContactsContract.Contacts.CONTENT_URI, values);
        String id = uri.getLastPathSegment();
        
        // 删除
        int deleted = resolver.delete(uri, null, null);
        assertEquals("应该删除1条记录", 1, deleted);
        
        // 验证已删除
        Cursor cursor = resolver.query(uri, null, null, null, null);
        assertEquals("删除后应该无数据", 0, cursor.getCount());
        cursor.close();
    }
}

我的经验:写 ContentProvider 测试时,一定要覆盖 Uri 匹配的边界情况。我曾经遇到过一个问题:传入的 Uri 多了一个斜杠,导致匹配失败,数据写不进去。测试帮我抓到了这个 bug。

另外,记得测试批量操作。联系人导入导出经常涉及批量插入,性能瓶颈往往在这里。

public void testBulkInsert() {
    ContentValues[] valuesArray = new ContentValues[100];
    for (int i = 0; i < 100; i++) {
        ContentValues values = new ContentValues();
        values.put(ContactsContract.Contacts.DISPLAY_NAME, "用户" + i);
        valuesArray[i] = values;
    }
    
    int inserted = resolver.bulkInsert(ContactsContract.Contacts.CONTENT_URI, valuesArray);
    assertEquals("批量插入数量不匹配", 100, inserted);
}

2. UI 测试:Espresso

UI 测试我首选 Espresso。它稳定、快速,而且和 Android 的 UI 线程配合得很好。

你想想看,联系人列表的滑动、点击、搜索,这些操作如果用手工测试,重复劳动不说,还容易漏掉边界情况。Espresso 可以帮你自动化。

@RunWith(AndroidJUnit4.class)
public class ContactListActivityTest {
    
    @Rule
    public ActivityTestRule<ContactListActivity> activityRule = 
        new ActivityTestRule<>(ContactListActivity.class);
    
    @Test
    public void testSearchContact() {
        // 在搜索框中输入关键字
        onView(withId(R.id.search_view))
            .perform(typeText("张三"), closeSoftKeyboard());
        
        // 验证搜索结果
        onView(withId(R.id.recycler_view))
            .check(matches(hasDescendant(withText("张三"))));
    }
    
    @Test
    public void testClickContactOpensDetail() {
        // 点击列表中的第一个联系人
        onView(withId(R.id.recycler_view))
            .perform(RecyclerViewActions.actionOnItemAtPosition(0, click()));
        
        // 验证跳转到详情页
        onView(withId(R.id.contact_detail_name))
            .check(matches(isDisplayed()));
    }
    
    @Test
    public void testAddContactButton() {
        // 点击添加按钮
        onView(withId(R.id.fab_add))
            .perform(click());
        
        // 验证弹出添加界面
        onView(withId(R.id.edit_name))
            .check(matches(isDisplayed()));
    }
}

注意:Espresso 测试中,如果涉及 RecyclerView 的操作,一定要用 RecyclerViewActions,不要直接用 onData。我见过有人用错了,结果测试跑起来各种报错。

还有一个坑:联系人列表可能很长,测试时不要假设列表已经加载完毕。用 IdlingResource 来等待异步加载完成。

@Test
public void testLoadMoreContacts() {
    // 注册 IdlingResource
    IdlingResource idlingResource = new RecyclerViewIdlingResource(
        activityRule.getActivity().findViewById(R.id.recycler_view));
    IdlingRegistry.getInstance().register(idlingResource);
    
    // 滑动到底部
    onView(withId(R.id.recycler_view))
        .perform(RecyclerViewActions.scrollToPosition(50));
    
    // 验证加载更多
    onView(withText("加载更多..."))
        .check(matches(isDisplayed()));
    
    // 注销
    IdlingRegistry.getInstance().unregister(idlingResource);
}

3. 性能测试:Profiler

性能测试,说白了就是看你的 App 跑起来卡不卡、耗不耗电、吃不吃内存。

Android Studio 自带的 Profiler 工具就够用了。我一般关注三个指标:CPU、内存、网络。

指标 关注点 联系人系统常见问题
CPU 主线程是否卡顿 联系人列表加载时,如果数据库查询在主线程执行,会导致掉帧
内存 是否有内存泄漏 联系人头像加载不当,容易造成 Bitmap 泄漏
网络 数据请求是否频繁 同步联系人时,如果每次变更都全量同步,网络开销很大

用 Profiler 抓一次性能数据,你就知道问题在哪了。我个人习惯在测试机上跑一遍完整的联系人操作流程:打开列表、搜索、点击详情、编辑、保存、返回。然后看 Profiler 的火焰图,找热点函数。

避坑指南:我曾经遇到一个性能问题:联系人列表滑动时明显卡顿。用 Profiler 一查,发现每次滑动都触发了数据库查询。后来加了缓存和分页,问题就解决了。所以性能测试一定要做,别凭感觉。

4. 兼容性测试:不同厂商 ROM

这是最头疼的部分。Android 碎片化严重,尤其是国内厂商的 ROM,各种魔改。

华为、小米、OPPO、vivo,每家都有自己的联系人管理方式。有的 ROM 改了 ContentProvider 的 Uri,有的改了权限模型,有的甚至把系统联系人应用给换了。

我总结了几点常见的兼容性问题:

  • 权限差异:部分 ROM 在 Android 6.0 以上仍然需要手动授权,否则读取联系人会返回空
  • ContentProvider 差异:有些 ROM 不支持某些查询参数,比如 LIMITOFFSET
  • UI 差异:系统联系人应用的样式不同,你的 App 如果调用系统界面,要注意适配
  • 同步机制:不同 ROM 的账户同步策略不同,可能导致联系人同步失败

我的建议:兼容性测试不要只依赖模拟器。找几台真机,覆盖主流厂商。我一般会准备华为、小米、OPPO 各一台,跑一遍核心用例。如果预算有限,至少用云测试平台跑一次。

另外,记得测试不同 Android 版本。Android 10 以后,联系人权限有了变化,READ_CONTACTS 不再是危险权限,而是变成了 READ_CONTACTSWRITE_CONTACTS 分开管理。你的代码要兼容这些变化。

// 兼容不同版本的权限检查
public boolean hasContactPermission(Context context) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        // Android 10 以上
        return ContextCompat.checkSelfPermission(context, 
            Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED;
    } else {
        // 低版本
        return ContextCompat.checkSelfPermission(context, 
            Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED;
    }
}

嗯,到这里,联系人系统的测试部分就讲完了。测试这件事,做的时候觉得麻烦,但上线后你就知道它的价值了。尤其是 ContentProvider 的单元测试和兼容性测试,能帮你省掉很多线上 bug。

总结一下:单元测试保数据正确,UI 测试保交互流畅,性能测试保体验不卡,兼容性测试保覆盖全面。四者缺一不可。


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