一、问题背景
刚开始用的是百度cafe搭建的框架,已经用了一些版本,最后的test版本在7.4的apk上能跑,但是在最新发布的7.5的版本上跑不了,直接提示nullPointer错误,通过打日志的方式发现local这个变量就是null,也就是说根本没有被初始化为正确的值,那么原因是什么呢?
二、问题排查
从两个方面进行排查:
1、我们的test程序:
因为本身用cafe搭建的已经有一段时间,而且当时因为用的是非root的手机,所以没有办法进行debug,所以就想本来cafe就是对robotium的一个封装,那就试试robotium的最简单的程序,看能不能找到问题原因。
从网上下载了robotium的最新的源码,notepad和notepadtest,这个导入过程也记录一下,下载下来的是一个zip的压缩包,里面包含两个工程,导入的时候,通过file-import-general-existing into workspace-next-select archive file,具体见下图:
然后Browse,找到正确的文件,确定finish即可。
官网给出的例子是有源码的,演变成没有源码的版本,NotePadTest中的主要内容如下:
/* * This is an example test project created in Eclipse to test NotePad which is a sample * project located in AndroidSDK/samples/android-11/NotePad * * * You can run these test cases either on the emulator or on device. Right click * the test project and select Run As --> Run As Android JUnit Test * * @author Renas Reda, renas.reda@robotium.com * */ package com.robotium.test; import com.robotium.solo.Solo; import android.test.ActivityInstrumentationTestCase2; public class NotePadTest extends ActivityInstrumentationTestCase2{ private Solo solo; private static String packageName = "com.example.android.notepad"; private static String mainActivity = "com.example.android.notepad.NotesList"; private static Class<?> launchableActivity; static{ try{ launchableActivity = Class.forName(mainActivity); }catch(ClassNotFoundException e){ throw new RuntimeException(e); } }; public NotePadTest(){ super(packageName, launchableActivity); } @Override public void setUp() throws Exception { //setUp() is run before a test case is started. //This is where the solo object is created. solo = new Solo(getInstrumentation(), getActivity()); } @Override public void tearDown() throws Exception { //tearDown() is run after a test case has finished. //finishOpenedActivities() will finish all the activities that have been opened during the test execution. solo.finishOpenedActivities(); } public void testAddNote() throws Exception { //Unlock the lock screen solo.unlockScreen(); solo.clickOnMenuItem("Add note"); //Assert that NoteEditor activity is opened solo.assertCurrentActivity("Expected NoteEditor activity", "NoteEditor"); //In text field 0, enter Note 1 solo.enterText(0, "Note 1"); solo.goBack(); //Clicks on menu item solo.clickOnMenuItem("Add note"); //In text field 0, type Note 2 solo.typeText(0, "Note 2"); //Go back to first activity solo.goBack(); //Takes a screenshot and saves it in "/sdcard/Robotium-Screenshots/". solo.takeScreenshot(); boolean notesFound = solo.searchText("Note 1") && solo.searchText("Note 2"); //Assert that Note 1 & Note 2 are found assertTrue("Note 1 and/or Note 2 are not found", notesFound); } public void testEditNote() throws Exception { // Click on the second list line solo.clickLongInList(2); solo.clickOnText("Edit title"); // Change orientation of activity solo.setActivityOrientation(Solo.LANDSCAPE); //In first text field (0), add test solo.enterText(0, " test"); //solo.goBack(); solo.setActivityOrientation(Solo.PORTRAIT); // (Regexp) case insensitive boolean noteFound = solo.waitForText("(?i).*?note 1 test"); //Assert that Note 1 test is found assertTrue("Note 1 test is not found", noteFound); } public void testRemoveNote() throws Exception { //(Regexp) case insensitive/text that contains "test" solo.clickOnText("(?i).*?test.*"); //Delete Note 1 test solo.clickOnMenuItem("Delete"); //Note 1 test should not be found boolean noteFound = solo.searchText("Note 1 test"); //Assert that Note 1 test is not found assertFalse("Note 1 Test is found", noteFound); solo.clickLongOnText("Note 2"); //Clicks on Delete in the context menu solo.clickOnText("Delete"); //Will wait 100 milliseconds for the text: "Note 2" noteFound = solo.waitForText("Note 2", 1, 100); //Assert that Note 2 is not found assertFalse("Note 2 is found", noteFound); } }
接下来我们的程序也按照类似这样的形式改造,单步调试(单步调试的快捷键请自行查找,我的是F7进函数,F8继续下一步),Run as Android junit test的情况下,发现需要手机root才可以,即需要在adb shell下能够显示#号才可以,因此我们采用了AVD进行调试,具体问题就是在setup的时候,solo = new Solo(getInstrumentation(), getActivity())方法,在第二个参数getActivity()这里,进入getActivity()方法,一步步往下调试的时候,发现卡在了下面方法中的加黄色的代码语句这里:
/** * Start a new activity and wait for it to begin running before returning. * In addition to being synchronous, this method as some semantic * differences from the standard {@link Context#startActivity} call: the * activity component is resolved before talking with the activity manager * (its class name is specified in the Intent that this method ultimately * starts), and it does not allow you to start activities that run in a * different process. In addition, if the given Intent resolves to * multiple activities, instead of displaying a dialog for the user to * select an activity, an exception will be thrown. * * <p>The function returns as soon as the activity goes idle following the * call to its {@link Activity#onCreate}. Generally this means it has gone * through the full initialization including {@link Activity#onResume} and * drawn and displayed its initial window. * * @param intent Description of the activity to start. * * @see Context#startActivity */ public Activity startActivitySync(Intent intent) { validateNotAppThread(); synchronized (mSync) { intent = new Intent(intent); ActivityInfo ai = intent.resolveActivityInfo( getTargetContext().getPackageManager(), 0); if (ai == null) { throw new RuntimeException("Unable to resolve activity for: " + intent); } String myProc = mThread.getProcessName(); if (!ai.processName.equals(myProc)) { // todo: if this intent is ambiguous, look here to see if // there is a single match that is in our package. throw new RuntimeException("Intent in process " + myProc + " resolved to different process " + ai.processName + ": " + intent); } intent.setComponent(new ComponentName( ai.applicationInfo.packageName, ai.name)); final ActivityWaiter aw = new ActivityWaiter(intent); if (mWaitingActivities == null) { mWaitingActivities = new ArrayList(); } mWaitingActivities.add(aw); getTargetContext().startActivity(intent); do { try { mSync.wait(); //就是卡在了这里 } catch (InterruptedException e) { } } while (mWaitingActivities.contains(aw)); return aw.activity; } }
以上这段代码如注释中所说:这个方法是启动一个Activity让其运行起来,加了同步锁的synchronized (mSync),然后走到了最下面的mSync.wait()这里,就一直等待,因为前面有一个内容挡住了这个wait,所以它一直等不到就一直出不了循环;只有等待成功,从do-while循环中出来,才能够启动并且将Activity返回。
在自己排查的过程中从网上找了一个帖子,具体见这里:http://www.2cto.com/kf/201410/344608.html,以及stackoverflow上的一个问题:http://stackoverflow.com/questions/20860832/why-does-getactivity-block-during-junit-test-when-custom-imageview-calls-start,虽然我卡住的地方跟他不同,但得到了一些启示,同时从robotium的源码中发现,getActivity()这个参数也可以去掉,因为我去掉这个方法后,在test程序安装之后,手动将程序启动起来,发现test能够正常run起来,不会出现挂起;然后就将手动启动程序的方式改成了通过调用方法自动启动程序的方法,见下方:
protected void startActivity(){ Intent it = new Intent(); it.setClassName(packageName, mainActivity); it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); getInstrumentation().getContext().startActivity(it); }
然后在solo的setup方法的new Solo()方法之前调用startActivity(),具体见下方:
@Before protected void setUp() throws Exception { startActivity(); solo = new Solo(getInstrumentation()); }
通过这种方法就可以暂时将case流程跑起来,但是在后面需要用到solo.waitForActivity("xxxx")以及solo.getCurrentActivity()方法也一样无法执行,这样的方式相当于放弃了很多solo中的比较好用的方法,所以需要再换一种思路。
上面的debug过程在7.4版本上重点过了一下,发现并没有卡在wait的位置,定位之后希望能从开发的代码上进行排查看能不能找到一些线索。
2、从开发的代码进行排查:
需要找到7.5与7.4的不同,确定修改了哪些内容可能导致出现这个问题,然而在你向另外一个人提问的时候,他一般会按照自己的理解说出自己认为的重点,并且不同模块由不同的人负责,甚至有些人会说不清自己改了什么,所以就找了开发组里对整个框架以及代码最熟悉的同事帮忙看这个问题。
三、与开发协调推进解决
因为手动启动并运行开发的程序是OK的,但是通过test程序run被测程序的时候就会根本跑不通基本流程,因此还是从最基本的内容查,包括LaunchActivity的相关,以及公共的一些跟启动相关的,开发哥哥在经过三天排查之后,终于发现了问题原因,注释掉一行插件初始化的代码就可以运行起来,加上就无法运行。
四、先把测试跑通
因为先自己本地down到代码,然后把存在问题的代码注释掉之后,先将测试跑起来。但是这个问题是因为robotium本身就是基于安卓底层去获取一些内容,在进行设置的时候,因为在同一个进程中,可能会与开发的一些调用方法或者设置存在冲突,排查起来还是需要大家一起努力(PS:前提是自己能够把问题定位出来)。
所以后来也用了appium跑了一下,结果能够跑通,appium基于client-server的机制,并不会出现以上的问题,那appium是否可能存在其他的问题呢?这个也需要再碰到再探讨。