30、联系人系统测试:单元测试、UI测试、性能测试与兼容性测试
说实话,写代码只是开发的一半。另一半,是证明你的代码真的能跑。
我见过太多项目,功能开发得飞起,一到测试阶段就崩。尤其是联系人这种系统级应用,数据敏感、交互频繁、还要适配各种ROM。你不把测试做扎实,上线就是给自己挖坑。
这一章,我把联系人系统的测试拆成四个维度来讲:单元测试(重点讲ContentProvider)、UI测试(Espresso)、性能测试(Profiler)、兼容性测试(不同厂商ROM)。每个维度我都会结合自己的踩坑经历,给你一些实用的建议。
核心观点:测试不是可有可无的环节,而是保障代码质量的最后一道防线。尤其是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 不支持某些查询参数,比如
LIMIT和OFFSET - UI 差异:系统联系人应用的样式不同,你的 App 如果调用系统界面,要注意适配
- 同步机制:不同 ROM 的账户同步策略不同,可能导致联系人同步失败
我的建议:兼容性测试不要只依赖模拟器。找几台真机,覆盖主流厂商。我一般会准备华为、小米、OPPO 各一台,跑一遍核心用例。如果预算有限,至少用云测试平台跑一次。
另外,记得测试不同 Android 版本。Android 10 以后,联系人权限有了变化,READ_CONTACTS 不再是危险权限,而是变成了 READ_CONTACTS 和 WRITE_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 测试保交互流畅,性能测试保体验不卡,兼容性测试保覆盖全面。四者缺一不可。