[Android]單元測試例項

大搜車-自娛發表於2013-11-05
轉自:http://yuanzhifei89.iteye.com/blog/1122104 作者:yuanzhifei

[b]測試相關資源 [/b]
讓開發自動化: 用 Eclipse 外掛提高程式碼質量[url=http://www.ibm.com/developerworks/cn/java/j-ap01117/index.html]http://www.ibm.com/developerworks/cn/java/j-ap01117/index.html[/url]

程式碼測試覆蓋率介紹:[url=http://www.cnblogs.com/coderzh/archive/2009/03/29/1424344.html]http://www.cnblogs.com/coderzh/archive/2009/03/29/1424344.html[/url]


[b]學習android單元測試時遇到的一些問題:[/b]
1.開始以為單元測試一定要從程式的launch Activity,一步一步的跳轉到所要測試的Activity能對其進行測試。
但實際上,我們可以從任意一個activity開始,對任意一個activity進行測試。

2.在執行單元測試之前,一定要先將要測試的程式安裝到模擬器或真機上。

[b]junit相關 [/b]
android中的測試框架是擴充套件的junit3,所以在學習android中的單元測試籤,可以先熟悉下junit3的使用,junit3要學習的東西應該並不多,就幾頁紙的東西。入門可以參考這個:[url=http://android.blog.51cto.com/268543/49994]http://android.blog.51cto.com/268543/49994 [/url]

[b]android單元測試框架中涉及的註解[/b]
@Suppress 可以用在類或這方法上,這樣該類或者該方法就不會被執行
@UiThreadTest 可以用在方法上,這樣該方法就會在程式的ui執行緒上執行
@LargeTest, @MediumTest, @SmallTest 用在方法上,標記所屬的測試型別,主要是用於單獨執行其中的某一類測試時使用。具體參考InstrumentationTestRunner類的文件。
@Smoke 具體用法還不清楚

[b]android單元測試框架中涉及的一些類的uml[/b]

[img]http://dl.iteye.com/upload/attachment/513766/4db7f7f4-80a0-3831-9013-0f23e79b69ba.png[/img]


[b]接下來我們以demo project來講解如何使用android中的單元測試[/b]
主要包括了三個activity:
MainActivity:僅包含一個button,點選後就可以進入LoginActivity

LoginActivity:可以輸入username, password,然後點選submit的話可進入HomeActivity,如果點選reset的話,輸入的內容就會被清空

HomeActivity:在TextView中顯示LoginActivity輸入的內容

[b]首先我們建立要測試的專案demo(使用了2.1)[/b]
[img]http://dl.iteye.com/upload/attachment/513754/799321ed-ed41-344d-a9dd-4dd0e7579cb3.png[/img]

[img]http://dl.iteye.com/upload/attachment/513756/c176e312-28fa-33c9-8370-347aa4298890.png[/img]

[img]http://dl.iteye.com/upload/attachment/513758/807f08b9-02be-3a49-bb49-248db3ccc4b8.png[/img]



[b]MainActivity程式碼 [/b]

public class MainActivity extends Activity {  
private static final boolean DEBUG = true;
private static final String TAG = "-- MainActivity";

@Override
protected void onCreate(Bundle savedInstanceState) {
if (DEBUG) {
Log.i(TAG, "onCreate");
}

super.onCreate(savedInstanceState);
setContentView(R.layout.act_main);
View toLoginView = findViewById(R.id.to_login);
toLoginView.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
if (DEBUG) {
Log.i(TAG, "toLoginView clicked");
}

Intent intent = new Intent(getApplicationContext(), LoginActivity.class);
startActivity(intent);
}
});
}
}


[b]MainActivity的佈局檔案[/b]

<LinearLayout  
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<Button
android:id="@+id/to_login"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:text="to login" />
</LinearLayout>


[img]http://dl.iteye.com/upload/attachment/513760/0011a959-1bbd-391b-9ef2-05453b68531e.png[/img]

[b]LoginActivity程式碼[/b]

public class LoginActivity extends Activity {
private static final boolean DEBUG = true;
private static final String TAG = "-- LoginActivity";

private EditText mUsernameView;
private EditText mPasswordView;

@Override
protected void onCreate(Bundle savedInstanceState) {
if (DEBUG) {
Log.i(TAG, "onCreate");
}

super.onCreate(savedInstanceState);
setContentView(R.layout.act_login);
mUsernameView = (EditText) findViewById(R.id.username);
mPasswordView = (EditText) findViewById(R.id.password);

View submitView = findViewById(R.id.submit);
submitView.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
if (DEBUG) {
Log.i(TAG, "submitView clicked");
}

Intent intent = new Intent(getApplicationContext(), HomeActivity.class);
intent.putExtra(HomeActivity.EXTRA_USERNAME, mUsernameView.getText().toString());
intent.putExtra(HomeActivity.EXTRA_PASSWORD, mPasswordView.getText().toString());
startActivity(intent);
}
});

View resetView = findViewById(R.id.reset);
resetView.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
if (DEBUG) {
Log.i(TAG, "resetView clicked");
}

mUsernameView.setText("");
mPasswordView.setText("");
mUsernameView.requestFocus();
}
});
}
}

[b]LoginActivity的佈局檔案[/b]

<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical"
>
<TextView
android:id="@+id/label_username"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="username:" />

<EditText
android:id="@+id/username"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:inputType="text" />

<TextView
android:id="@+id/label_password"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="password:" />

<EditText
android:id="@+id/password"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:inputType="textPassword" />

<Button
android:id="@+id/submit"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="submit" />

<Button
android:id="@+id/reset"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="reset" />
</LinearLayout>


[img]http://dl.iteye.com/upload/attachment/513764/f2410f24-991a-34a4-9cdc-b56bf0fe9148.png[/img]

[b]HomeActivity程式碼[/b]

public class HomeActivity extends Activity {
private static final boolean DEBUG = true;
private static final String TAG = "-- HomeActivity";

public static final String EXTRA_USERNAME = "yuan.activity.username";
public static final String EXTRA_PASSWORD = "yuan.activity.password";

@Override
protected void onCreate(Bundle savedInstanceState) {
if (DEBUG) {
Log.i(TAG, "onCreate");
}
super.onCreate(savedInstanceState);
Intent intent = getIntent();
StringBuilder sb = new StringBuilder();
sb.append("username:").append(intent.getStringExtra(EXTRA_USERNAME)).append("\n");
sb.append("password:").append(intent.getStringExtra(EXTRA_PASSWORD));

setContentView(R.layout.act_home);
TextView loginContentView = (TextView) findViewById(R.id.login_content);
loginContentView.setText(sb.toString());
}
}


[b]HomeActivity的佈局檔案 [/b]

<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<TextView
android:id="@+id/login_content"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="center"
android:textColor="#EEE"
android:textStyle="bold"
android:textSize="25sp" />
</LinearLayout>

[img]http://dl.iteye.com/upload/attachment/513762/27e986b0-fe63-349e-86b3-05a7560dc568.png[/img]

程式非常簡單,接下來我們為demo建立單元測試工程demo_unittest
[img]http://dl.iteye.com/upload/attachment/513748/326f401f-3546-3dfd-a100-b51eb6054304.png[/img]

選擇之前建立的工程demo,然後eclipse會自動幫我們設定api level,包名等。(測試用例的包名一般就是在要測試類的包名後加上test)
[img]http://dl.iteye.com/upload/attachment/513750/2aab55e5-f020-3ad8-b565-b9520cdb5bce.png[/img]
[img]http://dl.iteye.com/upload/attachment/513752/4dd2ec86-e033-3669-87d6-0206d127feba.png[/img]


建立完後eclipse會自動為我們建立好所需的目錄,Manifest.xml檔案
[img]http://dl.iteye.com/upload/attachment/513768/f47260f5-ef91-3aec-9478-9b7f4cb013bd.png[/img]

[b]接下來就是為要測試的類編寫測試用例。關於要用哪個測試用例類,在第一張UML圖中也做了簡要的說明。 [/b]
ActivityInstrumentationTestCase2:主要是用於進行activity的功能測試,和activity的互動測試,如activity間的跳轉,ui的互動等。

ActivityInstrumentationTestCase:這個類現在已deprecated了,所以不許考慮。

SingleLaunchActivityTestCase:該測試用例僅掉用setUp和tearDown一次,而不像其它測試用例類一樣,沒呼叫一次測試方法就會重新呼叫一次setUp和tearDown。所以主要測試activity是否能夠正確處理多次呼叫。

ActivityUnitTestCase:主要用於測試Activity,因為它允許注入MockContext和MockApplicaton,所以可以測試Activity在不同資源和應用下的情況。

還有Application等的測試用例比較簡單,看uml圖。如果覺得不夠詳細,可以參考sdk文件的dev guide和api reference。

[b]MainActivityTest測試用例[/b]

public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActivity> {
private static final String TAG = "=== MainActivityTest";

private Instrumentation mInstrument;
private MainActivity mActivity;
private View mToLoginView;

public MainActivityTest() {
super("yuan.activity", MainActivity.class);
}

@Override
public void setUp() throws Exception {
super.setUp();
mInstrument = getInstrumentation();
// 啟動被測試的Activity
mActivity = getActivity();
mToLoginView = mActivity.findViewById(yuan.activity.R.id.to_login);
}

public void testPreConditions() {
// 在執行測試之前,確保程式的重要物件已被初始化
assertTrue(mToLoginView != null);
}


//mInstrument.runOnMainSync(new Runnable() {
// public void run() {
// mToLoginView.requestFocus();
// mToLoginView.performClick();
// }
//});
@UiThreadTest
public void testToLogin() {
// @UiThreadTest註解使整個方法在UI執行緒上執行,等同於上面註解掉的程式碼
mToLoginView.requestFocus();
mToLoginView.performClick();
}

@Suppress
public void testNotCalled() {
// 使用了@Suppress註解的方法不會被測試
Log.i(TAG, "method 'testNotCalled' is called");
}

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



[b]LoginActivityTest測試用例 [/b]

public class LoginActivityTest extends ActivityInstrumentationTestCase2<LoginActivity> {
private static final String TAG = "=== LoginActivityTest";

private Instrumentation mInstrument;
private LoginActivity mActivity;
private EditText mUsernameView;
private EditText mPasswordView;
private View mSubmitView;
private View mResetView;

public LoginActivityTest() {
super("yuan.activity", LoginActivity.class);
}

@Override
public void setUp() throws Exception {
super.setUp();
/*
* 要向程式傳送key事件的話,必須在getActivity之前呼叫該方法來關閉touch模式
* 否則key事件會被忽略
*/
setActivityInitialTouchMode(false);

mInstrument = getInstrumentation();
mActivity = getActivity();
Log.i(TAG, "current activity: " + mActivity.getClass().getName());
mUsernameView = (EditText) mActivity.findViewById(yuan.activity.R.id.username);
mPasswordView = (EditText) mActivity.findViewById(yuan.activity.R.id.password);
mSubmitView = mActivity.findViewById(yuan.activity.R.id.submit);
mResetView = mActivity.findViewById(yuan.activity.R.id.reset);
}

public void testPreConditions() {
assertTrue(mUsernameView != null);
assertTrue(mPasswordView != null);
assertTrue(mSubmitView != null);
assertTrue(mResetView != null);
}

public void testInput() {
input();
assertEquals("yuan", mUsernameView.getText().toString());
assertEquals("1123", mPasswordView.getText().toString());
}

public void testSubmit() {
input();
mInstrument.runOnMainSync(new Runnable() {
public void run() {
mSubmitView.requestFocus();
mSubmitView.performClick();
}
});
}

public void testReset() {
input();
mInstrument.runOnMainSync(new Runnable() {
public void run() {
mResetView.requestFocus();
mResetView.performClick();
}
});
assertEquals("", mUsernameView.getText().toString());
assertEquals("", mPasswordView.getText().toString());
}

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

private void input() {
mActivity.runOnUiThread(new Runnable() {
public void run() {
mUsernameView.requestFocus();
}
});
// 因為測試用例執行在單獨的執行緒上,這裡最好要
// 同步application,等待其執行完後再執行
mInstrument.waitForIdleSync();
sendKeys(KeyEvent.KEYCODE_Y, KeyEvent.KEYCODE_U,
KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_N);

// 效果同上面sendKeys之前的程式碼
mInstrument.runOnMainSync(new Runnable() {
public void run() {
mPasswordView.requestFocus();
}
});
sendKeys(KeyEvent.KEYCODE_1, KeyEvent.KEYCODE_1,
KeyEvent.KEYCODE_2, KeyEvent.KEYCODE_3);
}
}


[b]HomeActivityTest測試用例[/b]

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");
// HomeActivity有extra引數,所以我們需要以intent來啟動它
mHomeActivity = launchActivityWithIntent("yuan.activity", HomeActivity.class, intent);
mLoginContentView = (TextView) mHomeActivity.findViewById(yuan.activity.R.id.login_content);
}

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

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



[b]接下來是執行測試用例,首先我們需要把要測試的程式安裝到模擬器或真機上[/b]

[img]http://dl.iteye.com/upload/attachment/513770/19f8fa10-06cf-3055-8b2a-9dac7dd9dd6c.png[/img]
[img]http://dl.iteye.com/upload/attachment/513772/b74099a9-6415-3784-9175-1bbaebc1477f.png[/img]


執行測試用例,檢視執行結果

[img]http://dl.iteye.com/upload/attachment/513774/6047f3b1-3044-3d5f-b3a7-509b3aebf909.png[/img]
[img]http://dl.iteye.com/upload/attachment/513776/96525d0f-c157-36b3-9c4a-da1796b40e6e.png[/img]


[b]這裡僅僅講了使用eclipse來進行單元測試,當然也是可以在命令列中進行單元測試的,但既然有eclipse這種圖形介面的工具,就不再折騰什麼命令列了。
還有就是測試用例也可以直接建立在源程式中(即原始碼和測試程式碼放在一個專案中),具體怎麼做的話google一些吧,就是把測試時涉及的一些Manifest元素移到原始碼工程的Manifest中等[/b]

相關文章