Robtinum的get Activity

1. 问题背景描述

在工作中需要在没有项目源码的情况下直接使用robotium测试目标android平台launcher,平台的版本基于当前最新的android 4.4.2。之前在验证可行性的时候使用本人同样使用android4.4.2的测试手机htc incredable s针对一个只有apk的notepad应用做过同样的验证,在测试手机上运行完全没有问题。该测试代码如下:

  1. package com.example.android.notepad.tryout;  
  2.   
  3. import com.robotium.solo.Solo;  
  4.   
  5. import android.test.ActivityInstrumentationTestCase2;  
  6. import android.widget.TextView;  
  7. import android.app.Activity;  
  8.   
  9. @SuppressWarnings("rawtypes")  
  10. public class NotePadTest extends ActivityInstrumentationTestCase2{  
  11.   
  12.     private static Solo solo = null;  
  13.     public Activity activity;  
  14.       
  15.     private static final int NUMBER_TOTAL_CASES = 2;  
  16.     private static int run = 0;  
  17.       
  18.     private static Class<?> launchActivityClass;  
  19.     //对应re-sign.jar生成出来的信息框里的两个值   
  20.     private static String mainActiviy = "com.example.android.notepad.NotesList";  
  21.     private static String packageName = "com.example.android.notepad";  
  22.   
  23.     static {  
  24.   
  25.         try {  
  26.   
  27.             launchActivityClass = Class.forName(mainActiviy);  
  28.   
  29.         } catch (ClassNotFoundException e) {  
  30.   
  31.             throw new RuntimeException(e);  
  32.   
  33.         }  
  34.   
  35.     }  
  36.       
  37.       
  38.     @SuppressWarnings("unchecked")  
  39.     public NotePadTest() {  
  40.         super(packageName, launchActivityClass);  
  41.     }  
  42.   
  43.       
  44.     @Override  
  45.     public void setUp() throws Exception {  
  46.         //setUp() is run before a test case is started.    
  47.         //This is where the solo object is created.   
  48.         super.setUp();   
  49.         //The variable solo has to be static, since every time after a case's finished, this class TCCreateNote would be re-instantiated   
  50.         // which would lead to soto to re-instantiated to be null if it's not set as static   
  51.         //TextView title = (TextView)getActivity().findViewById(Ref.id.title);   
  52.           
  53.         if(solo == null) {  
  54.               
  55.             NotePadTest.solo = new Solo(getInstrumentation(),getActivity());  
  56.               
  57.         }  
  58.     }  
  59.       
  60.     @Override  
  61.     public void tearDown() throws Exception {  
  62.         //Check whether it's the last case executed.   
  63.         run += countTestCases();  
  64.         if(run >= NUMBER_TOTAL_CASES) {  
  65.             solo.finishOpenedActivities();  
  66.         }  
  67.     }  
  68.   
  69.     public void testAddNoteCNTitle() throws Exception {  
  70.         //Thread.sleep(5000);   
  71.         solo.clickOnMenuItem("Add note");  
  72.         solo.enterText(0"中文标签笔记");  
  73.         solo.clickOnMenuItem("Save");  
  74.         solo.clickInList(0);  
  75.         solo.clearEditText(0);  
  76.         solo.enterText(0"Text 1");  
  77.         solo.clickOnMenuItem("Save");  
  78.         solo.assertCurrentActivity("Expected NotesList Activity""NotesList");  
  79.           
  80.         solo.clickLongOnText("中文标签笔记");  
  81.         solo.clickOnText("Delete");  
  82.           
  83.           
  84.     }  
  85.       
  86.       
  87.     public void testAddNoteEngTitle() throws Exception {  
  88.         solo.clickOnMenuItem("Add note");  
  89.         solo.enterText(0"English Title Note");  
  90.         solo.clickOnMenuItem("Save");  
  91.         solo.clickInList(0);  
  92.         solo.clearEditText(0);  
  93.         solo.enterText(0"Text 1");  
  94.         solo.clickOnMenuItem("Save");  
  95.         solo.assertCurrentActivity("Expected NotesList Activity""NotesList");  
  96.           
  97.         solo.clickLongOnText("English Title Note");  
  98.         solo.clickOnText("Delete");  
  99.     }  
  100. }  
package com.example.android.notepad.tryout;

import com.robotium.solo.Solo;

import android.test.ActivityInstrumentationTestCase2;
import android.widget.TextView;
import android.app.Activity;

@SuppressWarnings("rawtypes")
public class NotePadTest extends ActivityInstrumentationTestCase2{

	private static Solo solo = null;
	public Activity activity;
	
	private static final int NUMBER_TOTAL_CASES = 2;
	private static int run = 0;
	
	private static Class<?> launchActivityClass;
	//对应re-sign.jar生成出来的信息框里的两个值
	private static String mainActiviy = "com.example.android.notepad.NotesList";
	private static String packageName = "com.example.android.notepad";

	static {

		try {

			launchActivityClass = Class.forName(mainActiviy);

		} catch (ClassNotFoundException e) {

			throw new RuntimeException(e);

		}

	}
	
	
	@SuppressWarnings("unchecked")
	public NotePadTest() {
		super(packageName, launchActivityClass);
	}

	
	@Override
	public void setUp() throws Exception {
		//setUp() is run before a test case is started. 
		//This is where the solo object is created.
		super.setUp(); 
		//The variable solo has to be static, since every time after a case's finished, this class TCCreateNote would be re-instantiated
		// which would lead to soto to re-instantiated to be null if it's not set as static
		//TextView title = (TextView)getActivity().findViewById(Ref.id.title);
		
		if(solo == null) {
			
			NotePadTest.solo = new Solo(getInstrumentation(),getActivity());
			
		}
	}
	
	@Override
	public void tearDown() throws Exception {
		//Check whether it's the last case executed.
		run += countTestCases();
		if(run >= NUMBER_TOTAL_CASES) {
			solo.finishOpenedActivities();
		}
	}

	public void testAddNoteCNTitle() throws Exception {
		//Thread.sleep(5000);
		solo.clickOnMenuItem("Add note");
		solo.enterText(0, "中文标签笔记");
		solo.clickOnMenuItem("Save");
		solo.clickInList(0);
		solo.clearEditText(0);
		solo.enterText(0, "Text 1");
		solo.clickOnMenuItem("Save");
		solo.assertCurrentActivity("Expected NotesList Activity", "NotesList");
		
		solo.clickLongOnText("中文标签笔记");
		solo.clickOnText("Delete");
		
		
	}
	
	
	public void testAddNoteEngTitle() throws Exception {
		solo.clickOnMenuItem("Add note");
		solo.enterText(0, "English Title Note");
		solo.clickOnMenuItem("Save");
		solo.clickInList(0);
		solo.clearEditText(0);
		solo.enterText(0, "Text 1");
		solo.clickOnMenuItem("Save");
		solo.assertCurrentActivity("Expected NotesList Activity", "NotesList");
		
		solo.clickLongOnText("English Title Note");
		solo.clickOnText("Delete");
	}
}


但在工作的测试目标平台launcher中使用同样的方法去setup并运行简单的测试时碰到问题:测试程序一直挂起没有返回,程序挂起在以下getaActivity()方法(因是公司代码,故以notepad测试代码取代之):
  1. @Override  
  2. public void setUp() throws Exception {  
  3.     //setUp() is run before a test case is started.    
  4.     //This is where the solo object is created.   
  5.     super.setUp();   
  6.     //The variable solo has to be static, since every time after a case's finished, this class TCCreateNote would be re-instantiated   
  7.     // which would lead to soto to re-instantiated to be null if it's not set as static   
  8.       
  9.     if(solo == null) {        
  10.         NotePadTest.solo = new Solo(getInstrumentation(),getActivity());      
  11.     }  
  12. }  
	@Override
	public void setUp() throws Exception {
		//setUp() is run before a test case is started. 
		//This is where the solo object is created.
		super.setUp(); 
		//The variable solo has to be static, since every time after a case's finished, this class TCCreateNote would be re-instantiated
		// which would lead to soto to re-instantiated to be null if it's not set as static
		
		if(solo == null) {		
			NotePadTest.solo = new Solo(getInstrumentation(),getActivity());	
		}
	}

当时一直怀疑是否系统launcher的robotium初始化和setup方法跟普通的apk不一样,google上有历史文章描述getActivity()在Android 2.xx.xx上确实有这个问题,但后来的版本已经解决,而本人使用的时当前最的4.4.2,所以不应该还存在这种问题。针对这个思路去尝试找解决办法终无果。

2.问题分析

既然是getActvity()方法出现问题,而该方法原有的bug也已经在最新的版本fixed,在google无所获的情况下也只能剩下分析源码这条路了。因为是自己刚在backbook上搭建的自动化研究平台,为了节省时间,当时没有下载android的相应源码,只有sdk,所以第一步必须是先在项目中配置使用上android的源码,其理与配置javadoc相近,请查看本人之前的一篇文章《 How to Configure Javadoc for Robotium Library》,这里不做累术。

加入源码后调试分析,最终程序挂起在android.test.InstrumentationTestCase中的launchActivityWithIntent方法中,以下是eclipse中的调试截图示例:

以下是该方法的完整代码片段:
  1. /** 
  2.      * Utility method for launching an activity with a specific Intent. 
  3.      *  
  4.      * <p><b>NOTE:</b> The parameter <i>pkg</i> must refer to the package identifier of the 
  5.      * package hosting the activity to be launched, which is specified in the AndroidManifest.xml 
  6.      * file.  This is not necessarily the same as the java package name. 
  7.      * 
  8.      * @param pkg The package hosting the activity to be launched. 
  9.      * @param activityCls The activity class to launch. 
  10.      * @param intent The intent to launch with 
  11.      * @return The activity, or null if non launched. 
  12.      */  
  13.     @SuppressWarnings("unchecked")  
  14.     public final <T extends Activity> T launchActivityWithIntent(  
  15.             String pkg,  
  16.             Class<T> activityCls,  
  17.             Intent intent) {  
  18.         intent.setClassName(pkg, activityCls.getName());  
  19.         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);  
  20.         T activity = (T) getInstrumentation().startActivitySync(intent);  
  21.         getInstrumentation().waitForIdleSync();  
  22.         return activity;  
  23.     }  
/**
     * Utility method for launching an activity with a specific Intent.
     * 
     * <p><b>NOTE:</b> The parameter <i>pkg</i> must refer to the package identifier of the
     * package hosting the activity to be launched, which is specified in the AndroidManifest.xml
     * file.  This is not necessarily the same as the java package name.
     *
     * @param pkg The package hosting the activity to be launched.
     * @param activityCls The activity class to launch.
     * @param intent The intent to launch with
     * @return The activity, or null if non launched.
     */
    @SuppressWarnings("unchecked")
    public final <T extends Activity> T launchActivityWithIntent(
            String pkg,
            Class<T> activityCls,
            Intent intent) {
        intent.setClassName(pkg, activityCls.getName());
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        T activity = (T) getInstrumentation().startActivitySync(intent);
        getInstrumentation().waitForIdleSync();
        return activity;
    }

导致挂起的位置是里面的getInstrumentation().waitForIdleSync()方法,到了这里再代码跟踪进去看到的就是android.app.instrumentation这个基类里面:
  1. /** 
  2.  * Synchronously wait for the application to be idle.  Can not be called 
  3.  * from the main application thread -- use {@link #start} to execute 
  4.  * instrumentation in its own thread. 
  5.  */  
  6. public void waitForIdleSync() {  
  7.     validateNotAppThread();  
  8.     Idler idler = new Idler(null);  
  9.     mMessageQueue.addIdleHandler(idler);  
  10.     mThread.getHandler().post(new EmptyRunnable());  
  11.     idler.waitForIdle();  
  12. }  
    /**
     * Synchronously wait for the application to be idle.  Can not be called
     * from the main application thread -- use {@link #start} to execute
     * instrumentation in its own thread.
     */
    public void waitForIdleSync() {
        validateNotAppThread();
        Idler idler = new Idler(null);
        mMessageQueue.addIdleHandler(idler);
        mThread.getHandler().post(new EmptyRunnable());
        idler.waitForIdle();
    }
这里按照本人的理解做的事情大概如下:
  • 首先确保调用这个方法的来源不是application的主线程
  • 然后把当前等待application变成idle的请求放到消息队列中
  • 最后等待app在处理完所有事件达到idle状态的时候返回
看到这里我幡然领悟,在目标平台上面我们有一个天气预报的功能,在不停的发送事件给application(也就是launcher)来更新当前的天气情况,所以一直没有达到idle的状态,这样这个函数也就一直没有返回而挂起了。而在本人的测试手机上测试的notepad这个apk,一进去的launchable activity就是idle的,所以不会碰到这个问题。

带着这个思路在调整google关键字在stackoverflow中找到了国外同行碰到的一个类似的问题:http://stackoverflow.com/questions/20860832/why-does-getactivity-block-during-junit-test-when-custom-imageview-calls-start
这里总结下本人研究过程中了解到的robotium初始化solo的时候 new Solo(getInstrumentation(),getActivity())中getActivity所做的事情:
  • 如果目标activity没有起来,那么启动该activity并放在前台
  • 如果目标activity已经起来,那么直接放在前台等待被测试
  • 如果该该activity所属application在自动不停的接受事件,直接调用getActivity会因为一直等待application变成idle状态而挂起

3. 解决方法

本人按照项目中的目标测试launcher的实际情况想到的解决方法是在初始化solo的时候不去调用getActivity()这个InstrumentationTestCase2的方法:
  1. solo = new Solo(getInstrumentation());  
solo = new Solo(getInstrumentation());
因为我们的launcher在robotium在kill掉原来的launcher进程的时候就会自动起来,所以并不需要手动的去getActivity()去启动。这种方法在不能启动起来的apk如notepad上面就不行,不信你去掉getActivity()的调用,保证notepad不会启动或者放到前台。但是如果你在开始测试前先把notepad手动起来并放到前台,测试还是会正常进行的。比如以下的验证性代码:
  1. package com.example.android.notepad.tryout;  
  2.   
  3. import com.robotium.solo.Solo;  
  4.   
  5. import android.test.ActivityInstrumentationTestCase2;  
  6. import android.widget.TextView;  
  7. import android.app.Activity;  
  8.   
  9. @SuppressWarnings("rawtypes")  
  10. public class NotePadTest extends ActivityInstrumentationTestCase2{  
  11.   
  12.     private static Solo solo = null;  
  13.     public Activity activity;  
  14.       
  15.     private static final int NUMBER_TOTAL_CASES = 2;  
  16.     private static int run = 0;  
  17.       
  18.     private static Class<?> launchActivityClass;  
  19.     //对应re-sign.jar生成出来的信息框里的两个值   
  20.     private static String mainActiviy = "com.example.android.notepad.NotesList";  
  21.     private static String packageName = "com.example.android.notepad";  
  22.   
  23.     static {  
  24.   
  25.         try {  
  26.   
  27.             launchActivityClass = Class.forName(mainActiviy);  
  28.   
  29.         } catch (ClassNotFoundException e) {  
  30.   
  31.             throw new RuntimeException(e);  
  32.   
  33.         }  
  34.   
  35.     }  
  36.       
  37.       
  38.     @SuppressWarnings("unchecked")  
  39.     public NotePadTest() {  
  40.         super(packageName, launchActivityClass);  
  41.     }  
  42.   
  43.       
  44.     @Override  
  45.     public void setUp() throws Exception {  
  46.         //setUp() is run before a test case is started.    
  47.         //This is where the solo object is created.   
  48.         super.setUp();   
  49.         //The variable solo has to be static, since every time after a case's finished, this class TCCreateNote would be re-instantiated   
  50.         // which would lead to soto to re-instantiated to be null if it's not set as static   
  51.         //TextView title = (TextView)getActivity().findViewById(Ref.id.title);   
  52.           
  53.         if(solo == null) {  
  54.               
  55.             NotePadTest.solo = new Solo(getInstrumentation());//, getActivity());   
  56.               
  57.         }  
  58.     }  
  59.       
  60.     @Override  
  61.     public void tearDown() throws Exception {  
  62.         //Check whether it's the last case executed.   
  63.         run += countTestCases();  
  64.         if(run >= NUMBER_TOTAL_CASES) {  
  65.             solo.finishOpenedActivities();  
  66.         }  
  67.     }  
  68.   
  69.     public void testAddNoteCNTitle() throws Exception {  
  70.         //getActivity();   
  71.         Thread.sleep(5000);  
  72.         solo.clickOnMenuItem("Add note");  
  73.         solo.enterText(0"中文标签笔记");  
  74.         solo.clickOnMenuItem("Save");  
  75.         solo.clickInList(0);  
  76.         solo.clearEditText(0);  
  77.         solo.enterText(0"Text 1");  
  78.         solo.clickOnMenuItem("Save");  
  79.         solo.assertCurrentActivity("Expected NotesList Activity""NotesList");  
  80.           
  81.         solo.clickLongOnText("中文标签笔记");  
  82.         solo.clickOnText("Delete");  
  83.           
  84.           
  85.     }  
  86. }  
package com.example.android.notepad.tryout;

import com.robotium.solo.Solo;

import android.test.ActivityInstrumentationTestCase2;
import android.widget.TextView;
import android.app.Activity;

@SuppressWarnings("rawtypes")
public class NotePadTest extends ActivityInstrumentationTestCase2{

	private static Solo solo = null;
	public Activity activity;
	
	private static final int NUMBER_TOTAL_CASES = 2;
	private static int run = 0;
	
	private static Class<?> launchActivityClass;
	//对应re-sign.jar生成出来的信息框里的两个值
	private static String mainActiviy = "com.example.android.notepad.NotesList";
	private static String packageName = "com.example.android.notepad";

	static {

		try {

			launchActivityClass = Class.forName(mainActiviy);

		} catch (ClassNotFoundException e) {

			throw new RuntimeException(e);

		}

	}
	
	
	@SuppressWarnings("unchecked")
	public NotePadTest() {
		super(packageName, launchActivityClass);
	}

	
	@Override
	public void setUp() throws Exception {
		//setUp() is run before a test case is started. 
		//This is where the solo object is created.
		super.setUp(); 
		//The variable solo has to be static, since every time after a case's finished, this class TCCreateNote would be re-instantiated
		// which would lead to soto to re-instantiated to be null if it's not set as static
		//TextView title = (TextView)getActivity().findViewById(Ref.id.title);
		
		if(solo == null) {
			
			NotePadTest.solo = new Solo(getInstrumentation());//, getActivity());
			
		}
	}
	
	@Override
	public void tearDown() throws Exception {
		//Check whether it's the last case executed.
		run += countTestCases();
		if(run >= NUMBER_TOTAL_CASES) {
			solo.finishOpenedActivities();
		}
	}

	public void testAddNoteCNTitle() throws Exception {
		//getActivity();
		Thread.sleep(5000);
		solo.clickOnMenuItem("Add note");
		solo.enterText(0, "中文标签笔记");
		solo.clickOnMenuItem("Save");
		solo.clickInList(0);
		solo.clearEditText(0);
		solo.enterText(0, "Text 1");
		solo.clickOnMenuItem("Save");
		solo.assertCurrentActivity("Expected NotesList Activity", "NotesList");
		
		solo.clickLongOnText("中文标签笔记");
		solo.clickOnText("Delete");
		
		
	}
}
初始化solo的时候和testcase里面都没有去调用getActivity(),但是在testcase开始前先睡眠5秒,如果在这5秒的过程中你手动把notepad给启动起来,那么睡眠时间过后测试会继续正常运行。

刚才stackoverflow上提到的另外一个方法是重写getActivity()这个IntrumentationTestCase2的方法(注意我们所有的robotium测试类都是继承于该class的):
  1. @Override  
  2.     public MyActivity getActivity() {  
  3.         if (mActivity == null) {  
  4.             Intent intent = new Intent(getInstrumentation().getTargetContext(), MyActivity.class);  
  5.             intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);  
  6.             // register activity that need to be monitored.   
  7.             monitor = getInstrumentation().addMonitor(MyActivity.class.getName(), nullfalse);  
  8.             getInstrumentation().getTargetContext().startActivity(intent);  
  9.             mActivity = (MyActivity) getInstrumentation().waitForMonitor(monitor);  
  10.             setActivity(mActivity);  
  11.         }  
  12.         return mActivity;  
  13.     }  
@Override
    public MyActivity getActivity() {
        if (mActivity == null) {
            Intent intent = new Intent(getInstrumentation().getTargetContext(), MyActivity.class);
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            // register activity that need to be monitored.
            monitor = getInstrumentation().addMonitor(MyActivity.class.getName(), null, false);
            getInstrumentation().getTargetContext().startActivity(intent);
            mActivity = (MyActivity) getInstrumentation().waitForMonitor(monitor);
            setActivity(mActivity);
        }
        return mActivity;
    }
鉴于本人现在只是做前期的可行性研究,够用就好,且周末手头上也没有目标机器在手进行验证,所以有兴趣的朋友就自己去尝试下吧,

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值