Espresso介绍

在Android单元测试中,谷歌官方推荐使用Espresso框架,根据谷歌官方介绍,Espresso的最关键的优势就是它能自动同步模拟行为对UI的测试,它能够检测到主线程空闲状态的时候,以便在适当的时候运行你的测试代码或命令,这样你就没必要通过sleep去让主线程睡眠的方式去同步测试。说白了就是Espresso框架测试app不会通过阻塞主线程去同步UI测试。

Espresso有三种重要体系的类,分别是Matchers(匹配器),ViewAction(界面行为),ViewAssertions(界面判断),其中Matchers是常常是通过匹配条件来需找UI组件或过滤UI,而ViewAction是来模拟用户操作界面的行为,ViewAssertions对模拟行为操作的View进行变换和结果验证,其三者关系如图所示:


具体Espresso使用文档和Android 官方使用可以参考下面官方资料:

https://google.github.io/android-testing-support-library/docs/index.html

http://developer.android.com/training/testing/ui-testing/espresso-testing.html

Espresso实战

模拟用户行为测试:
代码:

package com.scau.beyondboy.idgoods;

import android.support.test.espresso.contrib.PickerActions;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.util.Log;
import android.widget.DatePicker;

import org.hamcrest.Matchers;
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.clearText;
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.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.withClassName;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;

/**
 * Author:beyondboy
 * Gmail:xuguoli.scau@gmail.com
 * Date: 2015-10-25
 * Time: 16:21
 */
//设置测试运行环境
@RunWith(AndroidJUnit4.class)
public class PersonInforTest
{
    private static final String TAG = PersonInforTest.class.getName();
    //设置启动的Activity
    @Rule
    public ActivityTestRule<MainActivity> mActivityTestRule=new ActivityTestRule<MainActivity>(MainActivity.class);
    //模拟用户的点击行为
    private void clicktest(final int id)
    {
        onView(withId(id)).perform(click());
    }
    //改变View的文本显示
    private String changetexttest(final int id,String text)
    {
        onView(withId(id)).perform(clearText(),typeText(text),closeSoftKeyboard());
        return text;
    }
    //检查View文本变化是否正确
    private void checktexttest(final int id,String text)
    {
        onView(withId(id)).check(matches(withText(text)));
    }

    //测试方法
   @Test
    public void NickNameTest()
   {
       clicktest(R.id.menu_toggle);
       clicktest(R.id.header_image);
       clicktest(R.id.nickname_layout);
       final String text=changetexttest(R.id.nickname,"test");
       clicktest(R.id.save);
       checktexttest(R.id.nickname, text);
   }

}

运行结果:

android Espresso如何运行 espresso安卓下载_Espresso测试


测试成功会显示绿条:

android Espresso如何运行 espresso安卓下载_android异步测_02


测试失败会显示红褐色条并报出对应的错误信息:

android Espresso如何运行 espresso安卓下载_android单元测_03


Espresso异步测试:

在开发中,我们经常会碰到异步请求,等异步请求完成后,再去更新UI,那么测试如何进行,估计很多测试新手可能会想到用Thread.sleep()方法让主线程睡眠等待,直到异步请求完成,刚开始我也是这么想的,但仔细想想这种方法是不对啊,我们知道主线程阻塞不能超过五秒,已超过五秒就会引起ANR异常,这种方式明显是不可行,那么有没有更好的方法呢?有,那就是通过Espresso的IdlingResource类来去完成,那是专门处理测试中的异步操作的发生,里面有两个相当重要的方法,其解释如下:

public void registerIdleTransitionCallback(ResourceCallback callback); 这个方法注册回调。

其回调接口:

public interface ResourceCallback {
    /**
     * Called when the resource goes from busy to idle.
     */
    public void onTransitionToIdle();
  }
}

public boolean isIdleNow(); 此方法通常用来通知主线程,其异步操作的完成,好让主线程更新UI,返回true便通知主线程去更新UI线程。
代码:

package com.scau.beyondboy.idgoods;

import android.support.test.espresso.IdlingResource;

import com.scau.beyondboy.idgoods.view.SlideListView;

/**
 * Author:beyondboy
 * Gmail:xuguoli.scau@gmail.com
 * Date: 2015-12-16
 * Time: 13:34
 * 实现ListView异步空闲处理类
 */
public class ListAdapterIdlingResource implements  IdlingResource
{
    private SlideListView mSlideListView;
    private IdlingResource.ResourceCallback mCallback;
    private final long startTime;
    private final long waitingTime;
    public  ListAdapterIdlingResource(long waitingTime,SlideListView slideListView)
    {
        this.startTime = System.currentTimeMillis();
        this.waitingTime = waitingTime;
        this.mSlideListView=slideListView;
    }

    @Override
    public String getName()
    {
        return "listadapterIdlingResource";
    }

    @Override
    public boolean isIdleNow()
    {
        //当网络数据加载完,才设置适配器,故可以通过适配器是否为空值来判断其异步数据加载是否完成
        if(mSlideListView.getAdapter()!=null)
        {
            mCallback.onTransitionToIdle();
            System.out.println("打印");
            return true;
        }
        return false;
        //通过时间来限制其异步加载
        /*long elapsed = System.currentTimeMillis() - startTime;
        boolean idle = (elapsed >= waitingTime);
        if (idle) {
            System.out.println("打印");
            mCallback.onTransitionToIdle();
        }
        return idle;*/
    }

    @Override
    public void registerIdleTransitionCallback(ResourceCallback callback)
    {
        this.mCallback=callback;
    }
}


package com.scau.beyondboy.idgoods;

import android.support.test.espresso.Espresso;
import android.support.test.espresso.IdlingPolicies;
import android.support.test.espresso.IdlingResource;
import android.support.test.espresso.matcher.BoundedMatcher;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.util.Log;

import com.scau.beyondboy.idgoods.view.SlideListView;

import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.Map;
import java.util.concurrent.TimeUnit;

import static android.support.test.espresso.Espresso.onData;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.Espresso.pressBack;
import static android.support.test.espresso.action.ViewActions.clearText;
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;
import static org.hamcrest.Matchers.anything;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasEntry;

/**
 * Author:beyondboy
 * Gmail:xuguoli.scau@gmail.com
 * Date: 2015-12-14
 * Time: 20:16
 */
@RunWith(AndroidJUnit4.class)
public class ProductTest
{
    private static final String TAG = ProductTest.class.getName();
    @Rule
    public ActivityTestRule<MainActivity> mActivityTestRule=new ActivityTestRule<MainActivity>(MainActivity.class);
    private void clicktest(final int id)
    {
        onView(withId(id)).perform(click());
    }

    private String changetexttest(final int id, String text)
    {
        onView(withId(id)).perform(clearText(),typeText(text),closeSoftKeyboard());
        return text;
    }

    private void checktexttest(final int id,String text)
    {
        onView(withId(id)).check(matches(withText(text)));
    }
    @Test    
    public void testClickOnItem()
    {
        clicktest(R.id.menu_toggle);
        clicktest(R.id.myproduct);
        int waitingTime=1000;
        //设置异步操作测试超时时间
        IdlingPolicies.setMasterPolicyTimeout(
                waitingTime * 10 TimeUnit.MILLISECONDS);
        IdlingPolicies.setIdlingResourceTimeout(
                waitingTime * 10, TimeUnit.MILLISECONDS);
        IdlingResource idlingResource=new ListAdapterIdlingResource(1000,(SlideListView)mActivityTestRule.getActivity().findViewById(R.id.product_slidelistview));
        //等待后台ListView加载完数据后执行后面的代码
        Espresso.registerIdlingResources(idlingResource);
        //选中一个listView的item选项           onData(anything()).inAdapterView(withId(R.id.product_slidelistview)).atPosition(10).perform(click());
        //释放对其异步空闲处理类
        Espresso.unregisterIdlingResources(idlingResource);
        clicktest(R.id.header_image);
        checktexttest(R.id.product_name, "其他的商品");
        pressBack();
        pressBack();        
    }    
}

运行结果如图:

android Espresso如何运行 espresso安卓下载_Espresso测试_04


Espresso Intent测试:

代码:

package com.scau.beyondboy.idgoods;

import android.support.test.espresso.intent.Intents;
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.intent.Intents.intended;
import static android.support.test.espresso.intent.matcher.IntentMatchers.hasComponent;
import static android.support.test.espresso.matcher.ViewMatchers.withId;

/**
 * Author:beyondboy
 * Gmail:xuguoli.scau@gmail.com
 * Date: 2016-01-07
 * Time: 12:57
 * 第一种Intent测试
 */
@RunWith(AndroidJUnit4.class)
@LargeTest
public class IntentTest
{
    @Rule
    public final ActivityTestRule<MainActivity> rule =
            new ActivityTestRule<>(MainActivity.class);   
    @Test
    public void intentTest()
    {
        //这种必须调用Intetns.init()方法
        Intents.init();
        clicktest(R.id.menu_toggle);
        clicktest(R.id.header_image);
        intended(hasComponent(PersonInfoActivity.class.getName()));
        clicktest(R.id.email_layout);
        intended(hasComponent(ChangeEmailActivity.class.getName()));
        Intents.release();
    }
    private void clicktest(final int id)
    {
        onView(withId(id)).perform(click());
    }
}




package com.scau.beyondboy.idgoods;

import android.support.test.espresso.intent.Intents;
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.intent.Intents.intended;
import static android.support.test.espresso.intent.matcher.IntentMatchers.hasComponent;
import static android.support.test.espresso.matcher.ViewMatchers.withId;

/**
 * Author:beyondboy
 * Gmail:xuguoli.scau@gmail.com
 * Date: 2016-01-07
 * Time: 12:57
 * 第二种Intent测试
 */
@RunWith(AndroidJUnit4.class)
@LargeTest
public class IntentTest
{    
    @Rule
   public IntentsTestRule<MainActivity> mRule = new IntentsTestRule<>(MainActivity.class);
    @Test
    public void intentTest()
    {       
        clicktest(R.id.menu_toggle);
        clicktest(R.id.header_image);
        intended(hasComponent(PersonInfoActivity.class.getName()));
        clicktest(R.id.email_layout);
        intended(hasComponent(ChangeEmailActivity.class.getName()));        
    }
    private void clicktest(final int id)
    {
        onView(withId(id)).perform(click());
    }
}

运行结果:

android Espresso如何运行 espresso安卓下载_ide_05


参考资料:

http://michaelevans.org/blog/2015/09/15/testing-intents-with-espresso-intents/

https://github.com/JakeWharton/double-espresso/tree/gradle/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp