Android測試系列之Instrumented Unit Test-Espresso
Instrumented unit tests are unit tests that run on physical devices and emulators, instead of the Java Virtual Machine (JVM) on your local machine.
這是源於Google API文件的一句話,它告訴我們Instrument Unit Test區別於Unit Test的地方就是它能夠在物理裝置和虛擬機器上執行。所以我們多來進行自動化的UI測試。
注意兩點:
第一、以下所有關於Instrumented Unit Test講解的測試環境都是在Android Studio
第二、android studio測試程式碼執行的Build Variants的Test ArtiFact的模式都是Android Instrumentation Test。
Google推薦了兩種自動化UI測試的方式,第一種是利用Espresso,這種方式適用於單個應用程式的測,測試環境是android 2.2(API 8)及其以上;第二種是UiAutoMator,這種方式適用於多個應用程式的測試,測試環境是android 4.3(API 18)及其以上。
在自動化測試的測試中:
1. 找到我們要測試的物件
2. 測試物件的動作
3. 檢查測試效果
下面我們學習這兩種自動化測試的時候的核心就是這兩步,只有熟悉了這兩種自動化測試實現的方式,以後的事情處理起來就的得心應手啦。
**
Testing UI for a Single App(Espresso)
**
建立一個Espresso測試用例,有以下步驟:
1.利用@Rule註解方式指明要測試的Activity
2. 呼叫OnView()或者OnData()方法找到我們要測試的UI控制元件
3. 通過呼叫theViewInteraction.perform()或DataInteraction.perform()方法模擬一個使用者操作
4. 使用ViewAssertions方法檢查UI反映預期的狀態或行為
下面就用兩個簡單的例子實現其測試功能:
第一個Demo:
Build.gradle
apply plugin : 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion "23.0.1"
defaultConfig {
applicationId "com.example.myapplication"
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
lintOptions {
abortOnError false
}
productFlavors {
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
// App dependencies
compile 'com.android.support:support-annotations:23.0.1'
compile 'com.google.guava:guava:18.0'
// Testing-only dependencies
// Force usage of support annotations in the test app, since it is internally used by the runner module.
androidTestCompile 'com.android.support:support-annotations:23.0.1'
androidTestCompile 'com.android.support.test:runner:0.4.1'
androidTestCompile 'com.android.support.test:rules:0.4.1'
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1'
testCompile 'junit:junit:4.12'
}
MainActivity.java
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
public class MainActivity extends Activity implements View.OnClickListener {
// The TextView used to display the message inside the Activity.
private TextView mTextView;
// The EditText where the user types the message.
private EditText mEditText;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Set the listeners for the buttons.
findViewById(R.id.changeTextBt).setOnClickListener(this);
findViewById(R.id.activityChangeTextBtn).setOnClickListener(this);
mTextView = (TextView) findViewById(R.id.textToBeChanged);
mEditText = (EditText) findViewById(R.id.editTextUserInput);
}
@Override
public void onClick(View view) {
// Get the text from the EditText view.
final String text = mEditText.getText().toString();
switch (view.getId()) {
case R.id.changeTextBt:
// First button's interaction: set a text in a text view.
mTextView.setText(text);
break;
case R.id.activityChangeTextBtn:
// Second button's interaction: start an activity and send a message to it.
Intent intent = ShowTextActivity.newStartIntent(this, text);
startActivity(intent);
break;
}
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="@dimen/activity_horizontal_margin"
tools:context=".MainActivity">
<TextView
android:id="@+id/textToBeChanged"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="@dimen/header_margin"
android:layout_marginTop="@dimen/header_margin"
android:text="@string/hello_world"
android:textAppearance="?android:attr/textAppearanceLarge"/>
<EditText
android:id="@+id/editTextUserInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:hint="@string/type_something"/>
<Button
style="?android:attr/buttonStyleSmall"
android:id="@+id/changeTextBt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/change_text"
android:layout_gravity="center_horizontal"/>
<Button
style="?android:attr/buttonStyleSmall"
android:id="@+id/activityChangeTextBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="@string/open_activity_and_change_text"/>
</LinearLayout>
ShowTextActivity.java
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.widget.TextView;
import com.google.common.base.Strings;
/**
* Created by Administrator on 16-2-23.
*/
public class ShowTextActivity extends Activity {
// The name of the extra data sent through an {@link Intent}.
public final static String KEY_EXTRA_MESSAGE =
"com.example.myapplication.MESSAGE";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_show_text);
// Get the message from the Intent.
Intent intent = getIntent();
String message = Strings.nullToEmpty(intent.getStringExtra(KEY_EXTRA_MESSAGE));
// Show message.
((TextView)findViewById(R.id.show_text_view)).setText(message);
}
/**
* Creates an {@link Intent} for {@link ShowTextActivity} with the message to be displayed.
* @param context the {@link Context} where the {@link Intent} will be used
* @param message a {@link String} with text to be displayed
* @return an {@link Intent} used to start {@link ShowTextActivity}
*/
static protected Intent newStartIntent(Context context, String message) {
Intent newIntent = new Intent(context, ShowTextActivity.class);
newIntent.putExtra(KEY_EXTRA_MESSAGE, message);
return newIntent;
}
}
activity_show_text.xml
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ShowTextActivity">
<TextView
android:id="@+id/show_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hello_world"
android:layout_gravity="center"/>
</merge>
叮叮叮噹,我們的主角出場,測試用例
ChangeTextBehaviorTest.java
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.LargeTest;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.action.ViewActions.closeSoftKeyboard;
import static android.support.test.espresso.action.ViewActions.typeText;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
/**
* Created by Administrator on 16-2-23.
*/
@RunWith(AndroidJUnit4.class)
@LargeTest
public class ChangeTextBehaviorTest {
public static final String STRING_TO_BE_TYPED = "espresso";
@Rule
public ActivityTestRule<MainActivity> mActivityRule=new ActivityTestRule<MainActivity>(MainActivity.class);
@Test
public void changeText_sameActivity(){
// Type text and then press the button.
onView(withId(R.id.editTextUserInput))
.perform(typeText(STRING_TO_BE_TYPED),closeSoftKeyboard());
onView(withId(R.id.changeTextBt))
.perform(click());
// Check that the text was changed.
onView(withId(R.id.textToBeChanged))
.check(matches(withText(STRING_TO_BE_TYPED)));
}
@Test
public void changeText_newActivity() {
// Type text and then press the button.
onView(withId(R.id.editTextUserInput)).perform(typeText(STRING_TO_BE_TYPED),
closeSoftKeyboard());
onView(withId(R.id.activityChangeTextBtn)).perform(click());
// This view is in a different Activity, no need to tell Espresso.
onView(withId(R.id.show_text_view)).check(matches(withText(STRING_TO_BE_TYPED)));
}
}
第二個Demo:
OtherActivity.java
import android.app.Activity;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ListView;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class OtherActivity extends Activity {
private List<Map<String,Object>> datas;
private MyAdapter myAdapter;
private ListView myListview;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_other);
initView();
initData();
setAdapter();
}
private void initView(){
myListview = (ListView) findViewById(R.id.my_lv);
}
private void initData(){
datas = new ArrayList<>();
for (int i = 0; i < 15; i++) {
Map<String,Object> map = new HashMap<>();
map.put("number","測試資料"+i);
datas.add(map);
}
}
private void setAdapter(){
myAdapter = new MyAdapter();
myListview.setAdapter(myAdapter);
}
class MyAdapter extends BaseAdapter{
@Override
public int getCount() {
return datas.size();
}
@Override
public Object getItem(int position) {
return datas.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView==null){
holder = new ViewHolder();
convertView= LayoutInflater.from(OtherActivity.this).inflate(R.layout.list_item,null);
holder.tv = (TextView) convertView.findViewById(R.id.rowContentTextView);
convertView.setTag(holder);
}else{
holder = (ViewHolder) convertView.getTag();
}
holder.tv.setText(datas.get(position).get("number").toString());
return convertView;
}
final class ViewHolder{
TextView tv;
}
}
}
activity_other.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/my_lv"
android:layout_width="match_parent"
android:layout_height="match_parent"></ListView>
</RelativeLayout>
叮叮叮噹,主角出場(注意裡面有hamcrest的應用,不瞭解的可以查查資料)
ListActivityTest.java
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import android.support.test.espresso.DataInteraction;
import android.support.test.espresso.Espresso;
import android.support.test.espresso.action.ViewActions;
import android.support.test.espresso.matcher.ViewMatchers;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.test.ActivityInstrumentationTestCase2;
import android.test.suitebuilder.annotation.LargeTest;
import java.util.Map;
import static android.support.test.espresso.Espresso.onData;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.assertion.ViewAssertions.doesNotExist;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.isChecked;
import static android.support.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasEntry;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
/**
* Created by Administrator on 16-2-25.
*/
@RunWith(AndroidJUnit4.class)
@LargeTest
public class LongListActivityTest {
private static final String TEXT_ITEM_30 = "item: 30";
private static final String TEXT_ITEM_30_SELECTED = "30";
private static final String TEXT_ITEM_60 = "item: 60";
// Match the last item by matching its text.
private static final String LAST_ITEM_ID = "測試資料14";
/**
* A JUnit {@link Rule @Rule} to launch your activity under test. This is a replacement
* for {@link ActivityInstrumentationTestCase2}.
* <p>
* Rules are interceptors which are executed for each test method and will run before
* any of your setup code in the {@link Before @Before} method.
* <p>
* {@link ActivityTestRule} will create and launch of the activity for you and also expose
* the activity under test. To get a reference to the activity you can use
* the {@link ActivityTestRule#getActivity()} method.
*/
@Rule
public ActivityTestRule<OtherActivity> mActivityRule = new ActivityTestRule<>(
OtherActivity.class);
/**
* Test that the list is long enough for this sample, the last item shouldn't appear.
*/
@Test
public void lastItem_NotDisplayed() {
// Last item should not exist if the list wasn't scrolled down.
onView(withText(LAST_ITEM_ID)).check(doesNotExist());
}
/**
* Check that the item is created. onData() takes care of scrolling.
*/
@Test
public void list_Scrolls() {
onRow(LAST_ITEM_ID).check(matches(isCompletelyDisplayed()));
}
/**
* Clicks on a row and checks that the activity detected the click.
*/
@Test
public void row_Click() {
// Click on one of the rows.
onRow(LAST_ITEM_ID).onChildView(withId(R.id.rowToggleButton)).perform(click());
// Check that the activity detected the click on the first column.
/* onView(ViewMatchers.withId(R.id.selection_row_value))
.check(matches(withText(TEXT_ITEM_30_SELECTED)));*/
}
/**
* Checks that a toggle button is checked after clicking on it.
*/
@Test
public void toggle_Click() {
// Click on a toggle button.
onRow(TEXT_ITEM_30).onChildView(withId(R.id.rowToggleButton)).perform(click());
// Check that the toggle button is checked.
onRow(TEXT_ITEM_30).onChildView(withId(R.id.rowToggleButton)).check(matches(isChecked()));
}
/**
* Make sure that clicking on the toggle button doesn't trigger a click on the row.
*/
@Test
public void toggle_ClickDoesntPropagate() {
// Click on one of the rows.
onRow(TEXT_ITEM_30).onChildView(withId(R.id.rowContentTextView)).perform(click());
// Click on the toggle button, in a different row.
onRow(TEXT_ITEM_60).onChildView(withId(R.id.rowToggleButton)).perform(click());
// Check that the activity didn't detect the click on the first column.
onView(ViewMatchers.withId(R.id.selection_row_value))
.check(matches(withText(TEXT_ITEM_30_SELECTED)));
}
/**
* Uses {@link Espresso#onData(org.hamcrest.Matcher)} to get a reference to a specific row.
* <p>
* Note: A custom matcher can be used to match the content and have more readable code.
* See the Custom Matcher Sample.
* </p>
*
* @param str the content of the field
* @return a {@link DataInteraction} referencing the row
*/
private static DataInteraction onRow(String str) {
return onData(hasEntry("number", str));
}
}
由於篇幅的原因,關於UiAutomator我就放在下一篇部落格吧!