忽然有一天,我想要做一件事:去代码中去验证那些曾经被“灌输”的理论。
-- 服装学院的IT男
1. 错误描述
新项目首轮全册出现CTS fail。
测试模块:CtsDevicePolicyManagerTestCases 测试case: com.android.cts.devicepolicy.ManagedProfileContactsTest#testManagedContactsUris com.android.cts.devicepolicy.ManagedProfileContactsTest#testManagedContactsPolicies
1.1 错误信息提取
错误信息堆栈很长,而且尝试从 testManagedContactsUris 方法开始分析的时候发现代码很多,难度很大。所以决定从报错的后面堆栈开始分析。
根据截图里的这段错误信息知道是在 testManagedProfileContactList_canListManagedContacts 开始出现断言失败,失败原因:期望返回2,结果返回 4
junit.framework.AssertionFailedError: expected:<2> but was:<4>
at junit.framework.Assert.fail(Assert.java:50)
at junit.framework.Assert.failNotEquals(Assert.java:287)
at junit.framework.Assert.assertEquals(Assert.java:67)
at junit.framework.Assert.assertEquals(Assert.java:199)
at junit.framework.Assert.assertEquals(Assert.java:205)
at com.android.cts.managedprofile.ContactsTest.testManagedProfileContactList_canListManagedContacts(ContactsTest.java:340)
2. 测试代码分析(第一轮)
直接从 ContactsTest 类下的 testManagedProfileContactList_canListManagedContacts 方法开始看
# ContactsTest
public void testManagedProfileContactList_canListManagedContacts() {
assertTrue(isManagedProfile());
// 获取集合
ArrayList<ContactInfo> contacts = getContactInfoFromListContacts(false /*isEnterprise*/);
// 错误信息报错行
assertEquals(2, contacts.size());
assertEquals(MANAGED_CONTACT_NAMES, mapToDisplayNames(contacts));
// When the managed profile queries itself, all contacts should have "normal" IDs
assertFalse(isEnterpriseContactId(contacts.get(0).contactId));
assertFalse(isEnterpriseContactId(contacts.get(1).contactId));
}
根据错误信息指向的是 “assertEquals(2, contacts.size());” 这一行。
看到这行说明方向是正确的,这里期望值是 2 ,但是 “contacts.size()” 的长度是 4 , 所以断言失败,导致测试失败。
而这个集合的返回来自 getContactInfoFromListContacts 方法,这边传递进去的参数为 false
# ContactsTest
private ArrayList<ContactInfo> getContactInfoFromListContacts(boolean isEnterprise) {
// 为false,所以取的是 Contacts.CONTENT_URI
Uri uri = isEnterprise ? Contacts.ENTERPRISE_CONTENT_URI : Contacts.CONTENT_URI;
return getAllContactsFromUri(uri, Contacts._ID, Contacts.DISPLAY_NAME_PRIMARY,
Contacts.PHOTO_URI, Contacts.PHOTO_THUMBNAIL_URI, Contacts.PHOTO_ID);
}
继续执行 getAllContactsFromUri ,这里的几个参数可以看到都是定义在 Contacts 下的一些常量,这些值感兴趣的可以自己看看,不知道也没事,不影响最终分析。
# ContactsTest
private ContentResolver mResolver;
private ArrayList<ContactInfo> getAllContactsFromUri(Uri uri, String idColumn,
String displayNameColumn, String photoUriColumn, String photoThumbnailColumn,
String photoIdColumn) {
Cursor cursor = mResolver.query(uri,
new String[] {
idColumn,
displayNameColumn,
photoUriColumn,
photoIdColumn,
photoThumbnailColumn,
}, null, null, null);
try (cursor) {
ArrayList<ContactInfo> allContacts = new ArrayList<>();
while (cursor != null && cursor.moveToNext()) {
allContacts.add(new ContactInfo(
cursor.getString(cursor.getColumnIndexOrThrow(idColumn)),
cursor.getString(cursor.getColumnIndexOrThrow(displayNameColumn)),
cursor.getString(cursor.getColumnIndexOrThrow(photoUriColumn)),
cursor.getString(cursor.getColumnIndexOrThrow(photoThumbnailColumn)),
cursor.getString(cursor.getColumnIndexOrThrow(photoIdColumn))));
}
return allContacts;
}
}
-
- 通过 ContentResolver 查询数据
-
- 定义一个返回集合 allContacts,内部存放 ContactInfo
-
- 把查询到的数据构建出一个 ContactInfo ,然后添加到返回值 allContacts 中
-
- 返回 allContacts
代码逻辑不复杂,测试逻辑到这其实就已经知道测试失败更具体的原因了,根据信息查询联系人,预期结果是查询到2个,但是实际上返回了4个。
知道问题fail根因后,下面要做的事情如下;
-
- 这里的结果是是哪 4 个联系人
-
- 哪2个是正确的,哪2个是多出来的
-
- 多出来的那2个联系人是哪来的
-
- 删除多出来的联系人,复测看是否能pass
3. 问题处理(第一轮)
3.1 在测试逻辑上加上log
返回的集合里放的是 ContactInfo ,这是在当前 ContactsTest 类下面定义的一个数据结构,先给他加上 toString ,为了便于看打印。
# ContactsTest
private class ContactInfo { // Not static to access outer world.
String contactId;
String displayName;
String photoUri;
String photoThumbnailUri;
String photoId;
......// 省略其他方法
// 加上toString
public String toString() {
return "ContactInfo{"
+ "contactId=" + contactId
+ ", displayName=" + displayName
+ ", photoUri=" + photoUri
+ ", photoThumbnailUri= " + photoThumbnailUri
+ ", photoId=" + photoId
+ '}';
}
}
然后在测试代码加上log,修改后代码如下:
# ContactsTest
private ContentResolver mResolver;
private ArrayList<ContactInfo> getAllContactsFromUri(Uri uri, String idColumn,
String displayNameColumn, String photoUriColumn, String photoThumbnailColumn,
String photoIdColumn) {
Cursor cursor = mResolver.query(uri,
new String[] {
idColumn,
displayNameColumn,
photoUriColumn,
photoIdColumn,
photoThumbnailColumn,
}, null, null, null);
try (cursor) {
ArrayList<ContactInfo> allContacts = new ArrayList<>();
while (cursor != null && cursor.moveToNext()) {
// 1. 构建数据的地方提出来
ContactInfo info = new ContactInfo(
cursor.getString(cursor.getColumnIndexOrThrow(idColumn)),
cursor.getString(cursor.getColumnIndexOrThrow(displayNameColumn)),
cursor.getString(cursor.getColumnIndexOrThrow(photoUriColumn)),
cursor.getString(cursor.getColumnIndexOrThrow(photoThumbnailColumn)),
cursor.getString(cursor.getColumnIndexOrThrow(photoIdColumn)));
// 2. 加上log
Log.i("biubiubiu", "allContacts.add "+info.toString());
// 3. 保持原逻辑,添加到集合
allContacts.add(info);
}
return allContacts;
}
}
目的就是把这个4个元素都打印出来,看看到底查出来哪4个联系人。
3.2 分析日志
把编译出来的文件替换后重新跑测试,获取 log 并分析
llContacts.add ContactInfo{contactId=1, displayName=XXXX Service Contacts, photoUri=null, photoThumbnailUri= null, photoId=null}
allContacts.add ContactInfo{contactId=2, displayName=XXXX Service Contacts, photoUri=null, photoThumbnailUri= null, photoId=null}
allContacts.add ContactInfo{contactId=3, displayName=Managed, photoUri=content://com.android.contacts/contacts/3/photo, photoThumbnailUri= content://com.android.contacts/contacts/3/photo, photoId=18}
allContacts.add ContactInfo{contactId=4, displayName=ManagedShared, photoUri=null, photoThumbnailUri= null, photoId=null}
可以看到确实是有4个数据,并且根据pass的设备对比,知道第3,4这两个联系人是期望的结果,前面2个联系人是多出来的。
前面的2个联系人数据由于项目原因,用“XXXX Service Contacts”代替。
3.3 解决方案(第一轮)
已经知道多出来的2个联系人的名字了,就根据信息去项目代码里找添加的地方。这一步如果不确定可以问公司负责 Contact 模块的人,为什么多出来2个预置的联系人。 我这边在项目的 Contact 目录下 grep 命令找到了预置这2个地方。然后将其移除后单编push,进行再跑 CTS 验证,结果为pass
本以为问题已经得到解决,于是找到Contact Owner 进行沟通,但是对方反馈预置联系人需求很正常,而且其他项目都有,但是 CTS 都是 pass 的,于是找了个也有预置联系人的项目验证,确实如此,所以还需要继续分析。
4. 测试代码分析(第二轮)
首先可以确认报错的原因还是测试用例查到的联系人多出来了2个预置的。 但是因为其他项目也预置了联系人,也能 pass ,那解决方案肯定不是把这个预置联系人的需求删除。在之前分析测试代码的时候是忽略了前面的调用链,直接从最后报错的地方开始看到,所以可能还忽略了其他的测试逻辑。
直接看后面这段报错的思路是没有问题的,也是最快速的,只不过当前 case 稍微复杂一点,但是也是在最快的视觉把问题的原因定位出来了。
现在分析一下前面的调用逻辑,这就要从 case 最开始执行的方法看了。
4.1 测试用例总结
testManagedContactsUris 这条 case 的代码还是很多的,根据方法名已经当前 fail 部分的分析,这条测试的目的还是很简单:
- 添加X个联系人
- 获取所有联系人
- 比较数量,是否一致
说到底就是我往联系人里添加了几个数据,我就希望查询到几个。
测试用例是通过 Content Provider 对数据库的增删改查来完成的。 带着这个宏观概念,开始梳理相关测试代码
4.2 分析
# ManagedProfileContactsTest
public void testManagedContactsUris() throws Exception {
runManagedContactsTest(() -> {
ContactsTestSet contactsTestSet = new ContactsTestSet(ManagedProfileContactsTest.this,
MANAGED_PROFILE_PKG, mParentUserId, mProfileUserId);
contactsTestSet.setCallerIdEnabled(true);
contactsTestSet.setContactsSearchEnabled(true);
// 本次分析的地方在这里,留个印象
contactsTestSet.checkIfCanListEnterpriseContacts(true);
contactsTestSet.checkIfCanLookupEnterpriseContacts(true);
contactsTestSet.checkIfCanFilterEnterpriseContacts(true);
contactsTestSet.checkIfCanFilterSelfContacts();
return null;
});
}
这一看就是 Lambda 表达式的形式,先看一下 runManagedContactsTest 方法的定义
# ManagedProfileContactsTest
private void runManagedContactsTest(Callable<Void> callable) throws Exception {
try {
// Allow cross profile contacts search.
// TODO test both on and off.
getDevice().executeShellCommand(
"settings put --user " + mProfileUserId
+ " secure managed_profile_contact_remote_search 1");
......
// Remove parent user's contacts to make sure the test starts from a clean state
// 1. 删除联系人
runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".ContactsTest",
"testRemoveAllUserContacts", mParentUserId);
......// 省略其他
// 2. 执行Lambda 表达式的内容
callable.call();
} finally {
// 3. 把测试用例添加的联系人删掉
// Clean up in managed profile and primary profile
runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".ContactsTest",
"testCurrentProfileContacts_removeContacts", mProfileUserId);
runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".ContactsTest",
"testCurrentProfileContacts_removeContacts", mParentUserId);
getDevice().uninstallPackage(DIRECTORY_PROVIDER_PKG);
}
}
testManagedContactsUris 这条 case 从这里看就感觉到方法很多,从上面的这段从的调用知道用例大的代码逻辑如下:
-
- runManagedContactsTest 方法先执行一堆初始化操作,其中有一个是执行 testRemoveAllUserContacts 方法先把联系人都删了
-
- 然后开始执行Lambda 表达式的部门,这里很多方法,都是具体的测试逻辑,其中会联系人有增删改查的操作,当前报错的是在 checkIfCanListEnterpriseContacts 方法内触发的
-
- 测试逻辑执行完后,把测试过程中添加的联系人都删除 (我觉得是想还原设备环境,但是只是删除了测试新增的,但是原来的联系人被删除了,没有加回来)
在测试命令端,和测试报告都可以看到以下输出:
可以确认在执行到 testManagedProfileContactList_canListManagedContacts 方法前,确实执行了 testRemoveAllUserContacts 方法,这个加上了日志也确实把预置联系人都删除了。
# ContactsTest
public void testRemoveAllUserContacts() throws RemoteException {
Log.i("biubiubiu", "testRemoveAllUserContacts before= "+getContactInfoFromListContacts(false).size());
for (ContactInfo contact : getContactInfoFromListContacts(false)) {
mResolver.delete(
Contacts.CONTENT_URI.buildUpon().appendPath(contact.contactId).build(), null);
}
Log.i("biubiubiu", "testRemoveAllUserContacts after= "+getContactInfoFromListContacts(false).size());
assertEquals(0, getContactInfoFromListContacts(false).size());
}
在首次执行case的时候,设备上有2个预置联系人,所以测试执行到这里时,先输出size为2,然后删除后就输出size为0 。
从这一段代码看,需求上预置联系人是不会导致 fail 的,因为测试前会执行删除。
在看报告中测试方法的执行顺序,最后一个 pass 的是 testPrimaryProfilePhoneList_canListOnlyPrimaryContacts 方法,看一下这个方法:
# ContactsTest
public void testPrimaryProfilePhoneList_canListOnlyPrimaryContacts() {
assertFalse(isManagedProfile());
// 注意这里是 getContactInfoFromListPhones
ArrayList<ContactInfo> contacts = getContactInfoFromListPhones(false /*isEnterprise*/);
assertEquals(2, contacts.size());
......
}
这个方法执行的是 getContactInfoFromListPhones 方法,和报错执行的不一样,所以再往上找。
# ContactsTest
public void testPrimaryProfileContactList_canListOnlyPrimaryContacts() {
assertFalse(isManagedProfile());
ArrayList<ContactInfo> contacts = getContactInfoFromListContacts(false /*isEnterprise*/);
Log.d("biubiubiu", "testPrimaryProfileContactList_canListOnlyPrimaryContacts befor "+ contacts.size());
assertEquals(2, contacts.size());
assertEquals(PRIMARY_CONTACT_NAMES, mapToDisplayNames(contacts));
assertFalse(isEnterpriseContactId(contacts.get(0).contactId));
assertFalse(isEnterpriseContactId(contacts.get(1).contactId));
Log.d("biubiubiu", "testPrimaryProfileContactList_canListOnlyPrimaryContacts end "+ contacts.size());
}
这个方法也是通过 getContactInfoFromListContacts 来获取联系人数量的,参数也是fail。
而且这里的断言的数量也是 2 ,和当前报错的方法是一致的。
也就是说先执行 testPrimaryProfileContactList_canListOnlyPrimaryContacts 再执行 testManagedProfileContactList_canListManagedContacts , 2个方法都是一样的 API 去获取联系人数量,并且断言的数量都是2。
现在是前者获取2,测试pass, 后续获取到数量为4, 测试fail。
分析到这里,就很奇怪,如果说有问题,那就是在这2个方法之间,又有某个地方重新把2个联系人加回来了。
于是从这期间找代码,找了很久,甚至还怀疑是 Content Provider 查询除了问题,还直接导出数据库来确认。
最终发现这条路是错误的。
4.3 多用户场景测试
查询同一个数据库,不可能在数据库没有改变的情况下,查询到的数据不一样,所以原因肯定是:查询的不是同一个数据库。
但是用的是同一个方法查询,那查询的肯定是同一个数据库,这个想法没问题,但是忽略了一个场景:多用户
前面分析代码的时候忽略了各个方法中还传递了用户ID这个参数。测试类 ManagedProfileContactsTest 的父类是 BaseManagedProfileTest 在 BaseManagedProfileTest 下面定义了很多变量,其中就有用户ID。
# BaseManagedProfileTest
// 主用户ID
protected int mParentUserId;
// 测试用户ID
protected int mProfileUserId;
这2个用户ID的赋值逻辑还是有一部分代码的,当前不展开分析,感兴趣的可以自己看。
正常情况下,mParentUserId 就是主用户ID,ID = 0 , mProfileUserId 就是当前为了测试,新建的一个用户。
在 ManagedProfileContactsTest::runManagedContactsTest 方法会先执行 runDeviceTestsAsUser 来删除已有联系人,保证测试环境的“干净”。但是这个方法传递了一个用户ID 作为参数
# ManagedProfileContactsTest
private void runManagedContactsTest(Callable<Void> callable) throws Exception {
......
// Remove parent user's contacts to make sure the test starts from a clean state
// 1. 删除联系人
runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".ContactsTest",
"testRemoveAllUserContacts", mParentUserId);
......// 省略其他
// 2. 执行Lambda 表达式的内容
callable.call();
......
}
注意这里传递的是 mParentUserId ,也就是说对主用户做了删除操作。
而 testManagedProfileContactList_canListManagedContacts 方法执行的时候,传递是子用户的 mProfileUserId
# ManagedProfileContactsTest
public void checkIfCanListEnterpriseContacts(boolean expected)
throws DeviceNotAvailableException {
// Primary user with standard list api can only list primary contacts
runDeviceTestsAsUser(mManagedProfilePackage, ".ContactsTest",
"testPrimaryProfileContactList_canListOnlyPrimaryContacts",
mParentUserId);
runDeviceTestsAsUser(mManagedProfilePackage, ".ContactsTest",
"testPrimaryProfilePhoneList_canListOnlyPrimaryContacts",
mParentUserId);
// Managed user with standard list api can only list managed contacts
runDeviceTestsAsUser(mManagedProfilePackage, ".ContactsTest",
"testManagedProfileContactList_canListManagedContacts",
mProfileUserId);
......
}
那么问题的本质就清晰了,删除的操作和查询的操作,在同一个用户下执行的,而用户直接数据是隔离的。所以当前 case fail 的原因是在新建用户下,多出来了2个预置联系人。
4.4 确认问题
前面是对代码的分析,根据代码的分析结果,
-
- 手动增加新用户并切换,观察是否有预置联系人。
-
- 在原套件代码增加对新增子用户的联系人删除,代码如下
# ManagedProfileContactsTest
private void runManagedContactsTest(Callable<Void> callable) throws Exception {
......
// Remove parent user's contacts to make sure the test starts from a clean state
// 1. 删除联系人
runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".ContactsTest",
"testRemoveAllUserContacts", mParentUserId);
// 新增对子用户的联系人删除
runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".ContactsTest",
"testRemoveAllUserContacts", mProfileUserId);
......// 省略其他
// 2. 执行Lambda 表达式的内容
callable.call();
......
}
测试结果:
-
- CTS pass 的项目切换后没有预置联系人,但是当前设备切换用户就已经有了预置联系人
-
- 修改套件后 pass
因此确认当前 case fail 的原因就是新增子用户的时候,不应该把主用户预置的联系人带过去,和之前项目保持一致。 当然也可以提 case 给 google,咨询是否可以按上面的方案修改,测试前也清空一下子用户的联系人数据。
5. 解决方案
测试逻辑:(统一用户下)根据条件查询联系人数量 预期结果:查询到2个联系人 实际结果: 查询到4个联系人
失败原因:测试过程中会创建子用户,而 google 测试前只删除了主用户的联系人,没有对子用户的联系人进行删除。 解决方案(二选一):
-
- 新增子用户时,不预置联系人
-
- 提case建议google对套件进行修改