Android UI 测试指南之 Espresso

栏目: Android · 发布时间: 7年前

内容简介:第一步.第二步.简单的登陆场景测试
  • Espresso 是一个简单好用的 Android UI 测试框架

  • Espresso 主要由以下三个基础部分组成:

    • ViewMatchers - 在当前View层级去匹配指定的View .
    • ViewActions - 执行Views的某些行为,如点击事件 .
    • ViewAssertions - 检查Views的某些状态,如是否显示 .
  • Espresso 使用示例

    onView(ViewMatcher) //1.匹配View
          .perform(ViewAction) //2.执行View行为
               .check(ViewAssertion); //3.验证View
    复制代码

准备

第一步. build.gradle 添加如下依赖:

androidTestImplementation 'com.android.support.test.espresso:espresso-core:latest.version'
androidTestImplementation 'com.android.support.test:runner:latest.version'
androidTestImplementation 'com.android.support.test:rules:latest.version'
复制代码

第二步. android.defaultConfig 添加如下配置

testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
复制代码

基础用法

获得View

  • withId方式
onView(withId(R.id.my_view))
复制代码
  • withText方式
onView(withText("Hello World!"))
复制代码

执行View行为

  • 点击
onView(...).perform(click());
复制代码
  • 文本内容输入
onView(...).perform(typeText("Hello"), click());
复制代码
  • scrollTo 滑动
onView(...).perform(scrollTo(), click());
复制代码

检验View

  • 检验View的文本内容
onView(...).check(matches(withText("Hello!")));
复制代码
  • 检验View的显示状态
onView(...).check(matches(isDisplayed()));
复制代码

示例场景:

简单的登陆场景测试

@RunWith(AndroidJUnit4.class)
public class LoginUITest {

    @Rule
    public ActivityTestRule<LoginActivity> rule=new ActivityTestRule<LoginActivity>(LogingActivity.class,true);

    @Test
    public void login(){
        //login
        onView(withId(R.id.userName)).perform(typeText("Jack"),closeSoftKeyboard());
        onView(withId(R.id.password)).perform(typeText("1234"),closeSoftKeyboard());
        onView(withText("登录")).perform(click());

        //verify
        onView(withId(R.id.content)).check(matches(isDisplayed()));
    }

}
复制代码

进阶用法

1. 使用IdlingResource

通常,我们实际的应用当中会有很多异步任务,例如网络请求,图片加载等,但是Espresso并不知道你的异步任务什么时候结束,所以需要借助于IdlingResource .

这里需要注意的是,如果你是通过AsyncTask或者AsyncTaskCompat方式的异步任务,Espresso已经处理好,并不需要去额外的处理。

第一步:添加依赖库

compile 'com.android.support.test.espresso:espresso-idling-resource:latest.version'
复制代码

第二步:定义一个 IdlingResource 接口 .

public interface IdlingResource {

  /**
   * 用来标识 IdlingResource 名称
   */
  public String getName();

  /**
   * 当前 IdlingResource 是否空闲 .
   */
  public boolean isIdleNow();

  /**
   注册一个空闲状态变换的ResourceCallback回调
   */
  public void registerIdleTransitionCallback(ResourceCallback callback);

  /**
   * 通知Espresso当前IdlingResource状态变换为空闲的回调接口
   */
  public interface ResourceCallback {
    /**
     * 当前状态转变为空闲时,调用该方法告诉Espresso
     */
    public void onTransitionToIdle();
  }
}
复制代码

下面我们以一个示例来说明: 场景:假设当前我们需要测试用户的头像的是否正常显示。

Activity 代码

public class AvatarActivity extends AppCompatActivity{

    private ImageView mAvatar;

    public static SimpleIdlingResource sIdlingResource=new SimpleIdlingResource();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_idling_resource);

        mAvatar= (ImageView) findViewById(R.id.avatar);

        //App 开始进入忙碌状态 ,等待通知
        sIdlingResource.increment();
        //开始加载头像图片
        Glide.with(this).load("https://avatars2.githubusercontent.com/u/2297803?v=3&s=460").into(new GlideDrawableImageViewTarget(mAvatar) {
            @Override
            public void onResourceReady(GlideDrawable resource,
                                        GlideAnimation<? super GlideDrawable> animation) {
                super.onResourceReady(resource, animation);
                //加载完毕后,将App设置成空闲状态
                sIdlingResource.decrement();
            }
        });
    }
}
复制代码

SimpleIdlingResource 代码

public final class SimpleIdlingResource implements IdlingResource {

    private final AtomicInteger counter = new AtomicInteger(0);

    private volatile ResourceCallback resourceCallback;

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

    /**
     * 如果counter的值等于0,说明当前是空闲状态
     */    
    @Override
    public boolean isIdleNow() {
        return counter.get() == 0;
    }

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

    /**
     * counter的值增长方法
     */
    public void increment() {
        counter.getAndIncrement();
    }

    /**
     *counter的值减少方法
     */
    public void decrement() {
        int counterVal = counter.decrementAndGet();
        if (counterVal == 0) {
            // 执行onTransitionToIdle()方法,告诉Espresso,当前是空闲状态。
            if (null != resourceCallback) {
                resourceCallback.onTransitionToIdle();
            }
        }

        if (counterVal < 0) {
            throw new IllegalArgumentException("Counter has been corrupted!");
        }
    }
}
复制代码

IdlingResourceTest 代码

@RunWith(AndroidJUnit4.class)
public class IdlingResourceTest {

    @Rule
    public ActivityTestRule<AvatarActivity> rule=new ActivityTestRule<>(AvatarActivity.class,true);

    @Before
    public void registerIdlingResource(){
        Espresso.registerIdlingResources(rule.getActivity().sIdlingResource);
    }

    @Test
    public void avatarDisplayed(){
        onView(withId(R.id.avatar)).check(matches(isDisplayed()));
    }

    @After
    public void unregisterIdlingResource() {
        Espresso.unregisterIdlingResources(
                rule.getActivity().sIdlingResource);
    }

}
复制代码

另外, Espresso 提供了一个实现好的 CountingIdlingResource 类,所以如果没有特别需求的话,直接使用 CountingIdlingResource 即可。

2.创建一个自定义 Espresso matcher

目前 Espresso 提供的方法基本上可以满足你的测试需求,如下图所示:

Android UI 测试指南之 Espresso

如果你需要对自定义的View中某个自定义属性进行测试的话,你可以创建一个自定义的Matcher

public static Matcher<View> isMoved() {
        return new TypeSafeMatcher<View>() {
            @Override
            public void describeTo(Description description) {
                description.appendText("is moved");
            }

            @Override
            public boolean matchesSafely(View view) {
                return ((CustomView)view).isMoved();
            }
        };
    }
复制代码

3.如何处理动画

  • 系统动画: 为了避免动画线程运行期间对 Espresso 测试产生的影响,官方强烈建议关闭系统动画。如图所示:
Android UI 测试指南之 Espresso
  • 自定义动画: 对于自定义动画,开发者可以借助以下代码去控制动画的开和关。

该方法可以监听系统动画的开关事件,这是一个值得推荐的做法,不仅仅是在Espresso测试中。

boolean animationEnabled=true;

    if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.JELLY_BEAN_MR1){
        try {
            if (Settings.Global.getFloat(getContentResolver(),Settings.Global.ANIMATOR_DURATION_SCALE)==0.0f){
                animationEnabled=false;
            }
        } catch (Settings.SettingNotFoundException e) {
            e.printStackTrace();
        }
    }

复制代码

4.优雅的 IntentTest

  • 普通的方式
@Rule
    public ActivityTestRule<MainActivity> rule=new ActivityTestRule<MainActivity>(MainActivity.class){

        @Override
        protected Intent getActivityIntent() {
            Intent result=new Intent(...);
            result.putExtra(...)
            return result;
        }
    };
复制代码

如果使用普通的方式,那么该单元测试类下的所有测试都是基于该 Intent 启动的 Activity ,这样显然不够灵活。

  • 优雅的方式
@Rule
    public ActivityTestRule<MainActivity> rule=new ActivityTestRule<MainActivity>(MainActivity.class,true,false);//第三个参数为是否自动运行Activity

    public void myTest(){
        Intent result=new Intent(...);
        result.putExtra(...);
        rule.launchActivity(result);
    }
复制代码

5.保证测试的独立性

通常使用Espresso进行UI测试的时候,你并不期望去测试网络或者远程的服务相关的东西,所以,你可以借助于 Espresso IntentMockito for mocking ,以及依赖注射、 Dagger2 。 总之,尽量去分离那些不属于UI层面的内容。

例如:你需要某个按钮的点击事件进行测试,但是该按钮的点击后,会跳转到别的 Activity ,可是你不希望去测试别的 Activity ,那么这里就可以通过拦截 Intent 来解决。

首先你需要添加IntentTest依赖库:

androidTestCompile 'com.android.support.test.espresso:espresso-intents:2.2.2'
复制代码

InterceptIntentTest 示例代码

@RunWith(AndroidJUnit4.class)
public class InterceptIntentTest {

    @Rule
    public IntentsTestRule<MainActivity> rule=new IntentsTestRule<>(MainActivity.class);

    @Before
    public void stubCameraIntent(){
        // 模拟一个ActivityResult
        Instrumentation.ActivityResult result = createImageCaptureActivityResultStub();
        //拦截MediaStore.ACTION_IMAGE_CAPTURE,并返回模拟后的result
        intending(hasAction(MediaStore.ACTION_IMAGE_CAPTURE)).respondWith(result);
    }

    @Test
    public void takePhoto_cameraIsLaunched(){
        onView(withId(R.id.button_take_photo)).perform(click());
        intended(hasAction(MediaStore.ACTION_IMAGE_CAPTURE));
        ...
    }

    private Instrumentation.ActivityResult createImageCaptureActivityResultStub() {
        // Create the ActivityResult, with a null Intent since we do not want to return any data
        // back to the Activity.
        return new Instrumentation.ActivityResult(Activity.RESULT_OK, null);
    }

}
复制代码

6.避免直接复制粘贴test代码

举个简单的例子。

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

以上这段代码是匹配列表中符合条件的item,并执行执行点击事件,测试也正常,同样这段代码也被复制到了其他的测试方法中使用, 这时,设想一下,如果你的 adapter 中的数据源改成了 cursor 或者其他,于是悲催了,你需要修改很多地方,显然,这不是一个合格的CV战士。

所以,我们需要对之前那行代码进行改装:

@Test
    public void myTest(){
        onData(withItemContent("item:50")).perform(click());
    }

    public static Matcher<? extends Object> withItemContent(String expectedText) {
        ....
    }

复制代码

很简单,只需将可能变化的部分抽出来即可。

7.如何测试View的位置

如图所示:

Android UI 测试指南之 Espresso

8.自定义错误日志

默认的错误日志打印信息比较多,如图:

Android UI 测试指南之 Espresso

如果你只想显示你关心的日志信息,你可以自定义FailureHandler:

private static class CustomFailureHandler implements FailureHandler {
    private final FailureHandler delegate;

    public CustomFailureHandler(Context targetContext) {
        delegate = new DefaultFailureHandler(targetContext);
    }

    @Override
    public void handle(Throwable error, Matcher<View> viewMatcher) {
        try {
            delegate.handle(error, viewMatcher);
        } catch (NoMatchingViewException e) {
            throw new MySpecialException(e);
        }
    }
}
复制代码
@Override
public void setUp() throws Exception {
    super.setUp();
    getActivity();
    setFailureHandler(new CustomFailureHandler(getInstrumentation()
                                              .getTargetContext()));
}
复制代码

以上所述就是小编给大家介绍的《Android UI 测试指南之 Espresso》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

The Intersectional Internet

The Intersectional Internet

Safiya Umoja Noble、Brendesha M. Tynes / Peter Lang Publishing / 2016

From race, sex, class, and culture, the multidisciplinary field of Internet studies needs theoretical and methodological approaches that allow us to question the organization of social relations that ......一起来看看 《The Intersectional Internet》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

MD5 加密
MD5 加密

MD5 加密工具

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具