最近在弄自动化测试的东西,用了google官方提供的espresso框架,用来测试UI,现在来讲下如何使用。

这是Espresso的介绍网址:http://developer.android.com/intl/zh-cn/training/testing/ui-testing/espresso-testing.html


Espresso源码地址:

git clone https://code.google.com/p/android-test-kit/



如何安装:

这是具体介绍:https://code.google.com/p/android-test-kit/wiki/EspressoSetupInstructions

我配置的结果:这是测试模块的build.gradle。

apply plugin: 'com.android.application'

android {
    compileSdkVersion 22
    buildToolsVersion "22"

    defaultConfig {
        applicationId "com.my.awesome.app"
        minSdkVersion 10
        targetSdkVersion 22.0.1
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }

    packagingOptions {
        exclude 'LICENSE.txt'
    }
}

dependencies {
    // App's dependencies, including test
    compile 'com.android.support:support-annotations:22.2.0'

    // Testing-only dependencies
    androidTestCompile 'com.android.support.test:runner:0.3'
    androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2'
}








有些涉及到项目具体的参数,就已经删去

使用Espresso有一点需要注意,如果需要使用键盘输入什么内容的话,建议将手机的默认输入法设置为google原生的输入法(英文),像类似小米这种手机虽然也提供google中文输入法,但恐怕被定制化过,所以还是去商店里下个原生的google键盘。我就遇到过用小米的输入法,Espresso出现错误。

开始编写测试用例:

如果是在junit3下,测试用例应该这么写

@LargeTest
public class HelloWorldEspressoTest extends ActivityInstrumentationTestCase2<MainActivity> {

    public HelloWorldEspressoTest() {
            super(MainActivity.class);
        }

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

    public void testListGoesOverTheFold() {
        onView(withText("Hello world!")).check(matches(isDisplayed()));
    }
}


如果是在junit4下,应该这么写

@RunWith(AndroidJUnit4.class)
@LargeTest
public class HelloWorldEspressoTest {

    @Rule
    public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule(MainActivity.class);

    public void listGoesOverTheFold() {
        onView(withText("Hello world!")).check(matches(isDisplayed()));
    }
}


这篇文章(http://googletesting.blogspot.com/2010/12/test-sizes.html)解释了Test size,分为large,small,medium,总结下就是

1.   Small: this test doesn't interact withany file system or network.
2.   Medium: Accesses file systems on boxwhich is running tests.
3.   Large: Accesses external file systems,networks, etc.



接下来就是espresso用到比较多的一些语法,官网的教程:https://code.google.com/p/android-test-kit/wiki/EspressoSamples

我这里是对他的“翻译”,这么解释比较好

Android Espresso 原理 espresso安卓下载_espresso

这是他的框架图。

onView函数是用来寻找控件的。里面可以通过id和内容等等来找

perform函数是用来执行一些操作的,比如说click和longclick的

check函数是用来判断的,比如说判断该控件是否显示isDisplay或者isEnabled等

onData函数是用来load数据的,用在list中比较多,这样讲不是很明显,等会儿就知道怎么用了


问题1:使用onView来找控件时,如果一个页面有多个控件使用同一个id,会出现如下exception


Android Espresso 原理 espresso安卓下载_espresso_02



解决办法

onView(allOf(withId(R.id.my_view),withText("Hello!")))
Youcan also use not to reverse any of the matchers:
onView(allOf(withId(R.id.my_view),not(withText("Unwanted"))))



问题2:在一个scrollview中,比如说有个button在很下面,你想点击该函数,你需要先将页面滑动到那个button那里,才能点击,类似的,onView只找当前显示在屏幕上的控件,如果找不到会抛出NoMatchingXXXXXXException。

解决办法

onView(...).perform(scrollTo(), click());


Note: scrollTowill have no effect if the view is already displayed so you can safely use itin cases when the view is displayed due to larger screen size (for example,when your tests run on both smaller and larger screen resolutions).



Espresso提供的action

https://code.google.com/p/android-test-kit/source/browse/espresso/lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/ViewActions.java




区别a view is not displayed和a view is not present

Not displayed:该控件在该页面上,但是不显示,可能是被滑出界面,也可能是gone或者invisible

onView(withId(R.id.bottom_left)).check(matches(not(isDisplayed())));

not present:该控件不存在,就类似findViewById找不到控件是一样的。

onView(withId(R.id.bottom_left)).check(doesNotExist());

 

 

assert是否包含该字符串

onView(..).check(withText(containsString(“hello world”)));

 

 

Matching a view next to another view(可以通过找在已知控件旁边的控件来确定目标控件)

A layout could contain certain views that are not unique bythemselves (e.g. a repeating call button in a table of contacts could have thesame R.id, contain the same text and have the same properties as other callbuttons within the view hierarchy) For example, in this activity, the view withtext "7" repeats across multiple rows:

Android Espresso 原理 espresso安卓下载_ide_03

Often, the non-unique view will be paired with some uniquelabel that's located next to it (e.g. a name of the contact next to the callbutton). In this case, you can use the hasSibling matcher to narrow down yourselection:

onView(allOf(withText("7"),hasSibling(withText("item:0")))).perform(click());

 

 

 

match如何定位到listview中的一项?Listview中都是由数据填充的

1.        如果该listview是List<Map>的数据,并且使用simpleAdapter。那么可以使用以下的方法来定位

onData(allOf(is(instanceOf(Map.class)), hasEntry(equalTo("STR"),is("item: 50"))).perform(click());

2.        如果是自己定义的数据结构比如JavaBean+ BaseAdapter怎么办? 

public class ItemData{
     public String name;
     public boolean isOpen;
 }

Adapter的getItem()要返回 data.get(i); 

 

private List<ItemData> data = new ArrayList<ItemData>();
 
@Override
 public Object getItem(int i) {
     return data.get(i);    // 这个就相当重要!!
 }

 

新写一个Matcher

import android.support.test.espresso.matcher.BoundedMatcher;
 import org.hamcrest.Description;
 import org.hamcrest.Matcher ;
 import static org.hamcrest.Matchers.equalTo;

     public static Matcher<Object>withName(String expectedText) {
         return withName( equalTo(expectedText));
     }

     public static Matcher<Object>withName( final Matcher<String> expectedObject) {
         returnnew BoundedMatcher<Object, ItemData>(ItemData. class) {
             @Override
             publicvoid describeTo(Description description) {
                description.appendText("with name: " + description);
             }

             @Override
             protectedboolean matchesSafely(ItemData itemData) {
                 return expectedObject .matches(itemData.name);
             }
         };
     }

  

 

如何检测list中是否包含该data item

private staticMatcher<View>withAdaptedData(finalMatcher<Object> matcher) {
     return new TypeSafeMatcher<View>(){

         @Override
         public void describeTo(Descriptiondescription) {
         }

         @Override
         public boolean matchesSafely(View view) {
             if (!(viewinstanceofAdapterView)) {
                 return false;
             }
             @SuppressWarnings("rawtypes")
             Adapter adapter =((AdapterView) view).getAdapter();
             for (inti =1;i < adapter.getCount()-1;i++) {
                 if (matcher.matches(adapter.getItem(i))) {
                     return true;
                 }
             }
             return false;
         }
     };
}

两个函数中,describeTo函数其实并没有什么用,而且如果你不实现,也并不会影响。主要起作用的是matchesSafely。

在测试用例中调用:

onView(withId(R.id.lv_payments_list)).check(matches(not(withAdaptedData(withItemContent("平安银行")))));

如果这里用try{}catch(AssertionFailedError e){}的话,如果list中有平安银行也不会报错。


Match一个view中的childView(比如点击listView中item里的控件)

onData(withName ("item: 60")).onChildView(withId(R.id.item_size)).perform(click());

  

识别是否是listView中的header或者footer

在activity中要这么写(斜体的内容很重要)

public static final String FOOTER = "FOOTER";
 ...
 View footer = layoutInflater.inflate(R.layout.list_item, listView, false);
 ((TextView)footer.findViewById(R.id.item_content)).setText("count:");
 ((TextView) footer.findViewById(R.id.size)).setText(data.size()+””);
listView.addFooterView(footerView,FOOTER, true);

再写一个matcher

import static org.hamcrest.Matchers.allOf;
 import static org.hamcrest.Matchers.instanceOf;
 import static org.hamcrest.Matchers.is;
 @SuppressWarnings("unchecked")
 public static Matcher<String> isFooter() {
   return allOf(is(instanceOf(String.class)), is(LongListActivity.FOOTER));
 }

在测试用例中这么写

import staticcom.google.android.apps.common.testing.ui.espresso.Espresso.onData;
 import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click;
import static com.google.android.apps.common.testing.ui.espresso.sample.LongListMatchers.isFooter;
 public void testClickFooter() {
   onData(isFooter())
     .perform(click());
   ...
 }

 

识别toast是否显示

<span style="font-size:18px;">onView(withText("test")).inRoot(withDecorView(not(is(actvRule.getActivity().getWindow().getDecorView())))).check(matches(isDisplayed()));</span>






使用自定义的异常处理器failure handler

privatestaticclassCustomFailureHandlerimplementsFailureHandler{
   private finalFailureHandlerdelegate;

   public CustomFailureHandler(Context targetContext){
     delegate =newDefaultFailureHandler(targetContext);
   }

   @Override
   public void handle(Throwable error,Matcher<View> viewMatcher){
     try {
       delegate.handle(error, viewMatcher);
     } catch(NoMatchingViewException e){
       throw newMySpecialException(e);
     }
   }
}

注册该异常处理器

@Override
public void setUp()throwsException{
   super.setUp();
   getActivity();
   setFailureHandler(newCustomFailureHandler(getInstrumentation().getTargetContext()));
}