9月份接手公司一个android的项目HYB,我主要负责了HYB中单元测试用例的编写.单元测试的编写保证了应用程序的健壮性以及可维护性.做了近一个月的android单元测试,有些心得体会和大家分享一下.

单元测试是在软件开发过程中,最低级别的测试活动,在该活动中软件的独立单元将在与程序的其他部分相隔离的情况下进行测试.android的单元测试其实也是一样的.

android中的测试框架是扩展的junit3,所以在学习android的单元测试之前,可以先学习junit3的使用.junit3要学习的东西其实也不多.

junit3的入门可以参考:http://android.blog.51cto.com/268543/49994

文档:http://wenku.baidu.com/view/87a176abd1f34693daef3e54.html

我在android的单元测试中所涉及到的相关类:

ActivityInstrumentationTestCase:初次做测试的话可以暂时不用考虑该类.

ActivityInstrumentationTestCase2:该类主要进行activity的功能测试和activity的交互测试.例如:Activity的跳转,UI的交互等.我用的最多的就是这个类.

SingleLaunchActivityTestCase:该测试用例仅调用setUp()和tearDown()一次,与其他测试用例不一样的是每调用一次测试方法,就会重新调用setUp()和tearDown().所以该类是为了测试activity是否能够正确处理多次调用.

ActivityUnitTestCase:主要用于测试Activity,因为它允许注入MockContext和MockApplicaton,所以可以测试Activity在不同资源和应用的情况.

具体的还可以参考sdk文档:dev guide和api reference.

下图就是android中与测试相关类的uml图:

Android 允许某个应用使用蓝牙 android允许在其他应用上显示_android

下面就拿一个具体的案例和大家讲解一下android中的单元测试是怎么实现的.首先需要说明的是这个案例是我在网上搜集来的,所以非常感谢博文:http://yuanzhifei89.iteye.com/blog/1122104

对该测试案例做一个简单的介绍:

Android 允许某个应用使用蓝牙 android允许在其他应用上显示_junit_02

包含三个Activity:MainActivity、HomeActivity、LoginActivity.

MainActivity:很简单就是一个Button,点击该Button进入到LoginActivity.

LoginActivity:四个可交互控件分别是:username、password、submit、reset

HomeActivity:显示LoginActivity提交过来的username、password

首先是demo这个android app的编写.

MainActivity的核心代码如下:

toLoginView.setOnClickListener(new View.OnClickListener() {

    @Override
public void onClick(View v) {    
        Intent intent = new Intent(getApplicationContext(),LoginActivity.class);
        startActivity(intent);
    }
});

LoginActivity的核心代码如下:

1     //declare widget
 2     private EditText mUsernameView;
 3     private EditText mPasswordView;
 4     
 5     /** Called when the activity is first created. */
 6     @Override
 7     protected void onCreate(Bundle savedInstanceState) {
 8         if(DEBUG) {
 9             Log.i(TAG,"LoginActivity's onCreate");
10         }
11         // TODO Auto-generated method stub
12         super.onCreate(savedInstanceState);
13         setContentView(R.layout.act_login);
14         
15         mUsernameView = (EditText)findViewById(R.id.username);
16         mPasswordView = (EditText)findViewById(R.id.password);
17         
18         View submitView = findViewById(R.id.submit);
19         submitView.setOnClickListener(new View.OnClickListener() {
20             @Override
21             public void onClick(View v) {
22                 // TODO Auto-generated method stub
23                 if(DEBUG) {
24                     Log.i(TAG,"submitView Clicked");
25                 }
26                 
27                 Intent intent = new Intent(getApplicationContext(), HomeActivity.class);
28                 intent.putExtra(HomeActivity.EXTRA_USERNAME, mUsernameView.getText().toString());
29                 intent.putExtra(HomeActivity.EXTRA_PASSWORD, mPasswordView.getText().toString());
30                 
31                 startActivity(intent);
32             }
33         });
34         
35         View resetView = findViewById(R.id.reset);
36         resetView.setOnClickListener(new View.OnClickListener() {
37             
38             @Override
39             public void onClick(View v) {
40                 // TODO Auto-generated method stub
41                 if(DEBUG) {
42                     Log.i(TAG,"resetView Clicked");
43                 }            
44                 mUsernameView.setText("");
45                 mPasswordView.setText("");
46                 mUsernameView.requestFocus();
47             }
48         });

为了方便大家,把LoginActivity的布局文件的代码也贴出来:

1 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 2     android:layout_width="fill_parent"  android:layout_height="fill_parent"
 3     android:orientation="vertical">
 4     <TextView
 5         android:id="@+id/label_username"
 6         android:layout_width="fill_parent"
 7         android:layout_height="wrap_content"
 8         android:text="username:" 
 9         />
10     <EditText
11         android:id="@+id/username"
12         android:layout_width="fill_parent"
13         android:layout_height="wrap_content"
14         android:inputType="text"
15         />
16     <TextView
17         android:id="@+id/label_password"
18         android:layout_width="fill_parent"
19         android:layout_height="wrap_content"
20         android:text="password:"
21         />
22     <EditText
23         android:id="@+id/password"
24         android:layout_width="fill_parent"
25         android:layout_height="wrap_content"
26         android:inputType="textPassword"
27         />
28     <Button
29         android:id="@+id/submit"
30         android:layout_width="fill_parent"
31         android:layout_height="wrap_content"
32         android:text="submit"
33         />  
34     <Button
35         android:id="@+id/reset"
36         android:layout_width="fill_parent"
37         android:layout_height="wrap_content"
38         android:text="reset"
39         />  
40 </LinearLayout>

最后就是HomeActivity了,很简单显示username和password就可以了,代码如下:

1 public class HomeActivity extends Activity {
 2     
 3     private static final boolean DEBUG = true;
 4     private static final String TAG = "--HomeActivity--";
 5     
 6     public static final String EXTRA_USERNAME = "com.ceo.demo.activity.username";
 7     public static final String EXTRA_PASSWORD = "com.ceo.demo.activity.password";
 8     
 9     @Override
10     protected void onCreate(Bundle savedInstanceState) {
11         if(DEBUG) {
12             Log.i(TAG,"HomeActivity's onCreate");
13         }
14         
15         // TODO Auto-generated method stub
16         super.onCreate(savedInstanceState);
17         setContentView(R.layout.act_home);
18         
19         Intent intent = getIntent();
20         StringBuilder sb = new StringBuilder();
21         sb.append("username:").append(intent.getStringExtra(EXTRA_USERNAME)).append("\n");
22         sb.append("password:").append(intent.getStringExtra(EXTRA_PASSWORD));
23         
24         TextView loginContentView = (TextView) findViewById(R.id.login_content);
25         loginContentView.setText(sb.toString());
26         
27     }
28 }

至此,demo应用程序编写好了,下面就是demo_unittest的编写了.

第一步:新建demo_unittest项目,和新建一个android项目的步骤基本类似.

Android 允许某个应用使用蓝牙 android允许在其他应用上显示_Text_03

单击之后,弹出New Android Unit Test的对话框

Test Project Name:填写demo_unittest  application name 也是这个

An existing Android Project:选择需要编写测试用例的项目----->这里我们选择demo

版本的选择,最好是和原项目的版本一致,避免不必要的错误.

这里需要注意一点就是包名的取名,最好是在原有项目的包名之后加上.test

点击finish之后,系统会自动为我们创建所需要的目录等信息.非常方便.

接下来可以选择AndroidManifest.xml文件,查看一下里面的构造,你会发现:

1 <?xml version="1.0" encoding="utf-8"?>
 2 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
 3       package="com.ceo.demo.activity.test"
 4       android:versionCode="1"
 5       android:versionName="1.0">
 6     <application android:icon="@drawable/icon" android:label="@string/app_name">
 7 
 8     <uses-library android:name="android.test.runner" />
 9     </application>
10     <uses-sdk android:minSdkVersion="8" />
11     <instrumentation android:targetPackage="com.ceo.demo.activity" android:name="android.test.InstrumentationTestRunner" />
12 </manifest>

系统自动帮我们添加了所需属性.

接下来就是测试类的编写:首先是MainActivityTest:

1 public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActivity> {
 2     //private static final String TAG = "=== MainActivityTest";
 3     
 4     @SuppressWarnings("unused")
 5     private Instrumentation mInstrument;
 6     private MainActivity mActivity;
 7     private View mToLoginView;
 8     
 9     public MainActivityTest() {
10         super("com.ceo.demo.activity", MainActivity.class);
11     }
12 
13     @Override
14     protected void setUp() throws Exception {
15         super.setUp();
16         
17         mInstrument = getInstrumentation();
18         // 启动被测试的Activity
19         mActivity = getActivity();
20         mToLoginView = mActivity.findViewById(com.ceo.demo.activity.R.id.to_login);
21     }
22     
23     public void testPreConditions() {
24         // 在执行测试之前,确保程序的重要对象已被初始化
25         assertTrue(mToLoginView != null);
26     }
27     
28     //@UiThreadTest 可以用在方法上,这样该方法就会在程序的ui线程上执行
29     @UiThreadTest  
30     public void testToLogin() {   
31         // @UiThreadTest注解使整个方法在UI线程上执行  
32         mToLoginView.requestFocus();   
33         mToLoginView.performClick();   
34     }
35 
36     @Override
37     protected void tearDown() throws Exception {
38         
39         super.tearDown();
40     }
41     
42 }

最主要的还是LoginActivityTest的编写,比较有代表性.

1 public class LoginActivityTest extends ActivityInstrumentationTestCase2<LoginActivity> {
 2     private static final String TAG = "=== LoginActivityTest";
 3     
 4     private Instrumentation mInstrument;
 5     private LoginActivity mActivity;
 6     private EditText mUsernameView;
 7     private EditText mPasswordView;
 8     private View mSubmitView;
 9     private View mResetView;
10     
11     public LoginActivityTest() {
12         super("com.ceo.demo.activity", LoginActivity.class);
13     }
14 
15     @Override
16     protected void setUp() throws Exception {
17         super.setUp();
18         /*  
19          *  要向程序发送key事件的话,必须在getActivity之前调用该方法来关闭touch模式  
20          *  否则key事件会被忽略  
21          */  
22         setActivityInitialTouchMode(false);
23         mInstrument = getInstrumentation();
24         mActivity = getActivity();
25         Log.i(TAG, "current activity: " + mActivity.getClass().getName());
26         
27         mUsernameView = (EditText) mActivity.findViewById(com.ceo.demo.activity.R.id.username);
28         mPasswordView = (EditText) mActivity.findViewById(com.ceo.demo.activity.R.id.password);
29         mSubmitView = mActivity.findViewById(com.ceo.demo.activity.R.id.submit);   
30         mResetView = mActivity.findViewById(com.ceo.demo.activity.R.id.reset);
31         
32     }
33     
34     public void testPreConditions() {   
35         assertTrue(mUsernameView != null);   
36         assertTrue(mPasswordView != null);   
37         assertTrue(mSubmitView != null);   
38         assertTrue(mResetView != null);   
39     }
40     
41     public void testInput() {   
42         input();   
43         assertEquals("yuan", mUsernameView.getText().toString());   
44         assertEquals("1123", mPasswordView.getText().toString());   
45     }
46     
47     public void testSubmit() {   
48         input();   
49         mInstrument.runOnMainSync(new Runnable() {   
50             public void run() {   
51                 mSubmitView.requestFocus();   
52                 mSubmitView.performClick();   
53             }   
54         });   
55     }   
56   
57     public void testReset() {   
58         input();   
59         mInstrument.runOnMainSync(new Runnable() {   
60             public void run() {   
61                 mResetView.requestFocus();   
62                 mResetView.performClick();   
63             }   
64         });   
65         assertEquals("", mUsernameView.getText().toString());   
66         assertEquals("", mPasswordView.getText().toString());   
67     }   
68   
69     @Override  
70     public void tearDown() throws Exception {   
71         super.tearDown();   
72     }   
73 
74     private void input() {   
75         mActivity.runOnUiThread(new Runnable() {   
76             public void run() {   
77                 mUsernameView.requestFocus();   
78             }   
79         });   
80         // 因为测试用例运行在单独的线程上,这里最好要   
81         // 同步application,等待其执行完后再运行   
82         mInstrument.waitForIdleSync();   
83         sendKeys(KeyEvent.KEYCODE_Y, KeyEvent.KEYCODE_U,   
84                 KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_N);   
85   
86         // 效果同上面sendKeys之前的代码   
87         mInstrument.runOnMainSync(new Runnable() {   
88             public void run() {   
89                 mPasswordView.requestFocus();   
90             }   
91         });   
92         sendKeys(KeyEvent.KEYCODE_1, KeyEvent.KEYCODE_1,   
93                 KeyEvent.KEYCODE_2, KeyEvent.KEYCODE_3);   
94     }
95 }

HomeActivity的编写:

public class HomeActivityTest extends ActivityUnitTestCase<HomeActivity> {
//private static final String TAG = "=== HomeActivityTest";

private static final String LOGIN_CONTENT = "username:yuan\npassword:1123";

private HomeActivity mHomeActivity;
private TextView mLoginContentView;

public HomeActivityTest() {
super(HomeActivity.class);
    }

    @Override
public void setUp() throws Exception {
super.setUp();
        Intent intent = new Intent();
        intent.putExtra(HomeActivity.EXTRA_USERNAME, "yuan");
        intent.putExtra(HomeActivity.EXTRA_PASSWORD, "1123");
        mHomeActivity = launchActivityWithIntent("com.ceo.demo.activity", HomeActivity.class, intent);
        mLoginContentView = (TextView) mHomeActivity.findViewById(com.ceo.demo.activity.R.id.login_content);
    }

public void testLoginContent() {
        assertEquals(LOGIN_CONTENT, mLoginContentView.getText().toString());
    }

    @Override
public void tearDown() throws Exception {
super.tearDown();
    }
}

最后就是检验成果的时候了:

右击项目的包名,run as Android Junit Test

Android 允许某个应用使用蓝牙 android允许在其他应用上显示_Text_04

绿色全线通过.

再次感谢博客:http://yuanzhifei89.iteye.com/blog/1122104案例的提供.

注意事项:

#1.如果没有预先安装项目的话,在做单元测试的时候会先安装项目到您的模拟器或者真机上,然后再执行单元测试.

#2.对Activity的测试,不一定非要从程序的Launch Activity开始,一步一步往下执行,可以从任意一个需要测试的Activity开始执行.

#3.在测试类中怎样获取控件:toLoginView是一个Button,View toLoginView = findViewById(R.id.to_login);

#4.从一个Activity启动另一个Activity,Intent intent = new Intent(getApplicationContext(),LoginActivity.class);

#5.获得当前的Activity再调用Activity的add方法--->最后进行预期值和实际值的判断,assertEquals(5, getActivity().add(2, 3));

#6.直接在UI线程中执行模拟事件,容易造成UI线程堵塞,解决方案在后面的博文中给出.

#7.protected void setUp()----->Sets up the fixture, for example, open a network connection. This method is called before a test is executed.

#8.protected void tearDown()----->Make sure all resources are cleaned up and garbage collected before moving on to the next test. Subclasses that override this method should make sure they call super.tearDown() at the end of the overriding method.

#9.setUp()用来初始设置。例如:启动Activity、初始化资源等,tearDown()用来垃圾的清理以及资源的回收等。例如:关闭数据库、finish Activity等