Espresso之onView()&onData()

如果你看了上一篇中对一些常用方法的介绍,想必现在已经可以对Espresso正常进行使用了,从这里开始我们开始看一些“简单”的东西。了解一下这三部曲是什么。

onView(ViewMatcher)
    .perform(ViewAction)
    .check(ViewAssertion);

从onView()&onData()开始

作为一个程序猿自然不能满足于三部曲,虽然不能什么都一清二楚,但是最差也要知道大概的流程吧。不然都不好意思说自己用过Espresso。所以与 Espresso 的故事就是从 Ctrl 打开 onView()开始了。
这里直接进入 Espresso 类, 这里主要有几个常用的静态工具函数

函数名 功能
pressBack() 返回键
closeSoftKeyboard() 关闭软键盘
openActionBarOverflowOrOptionsMenu() 菜单键
openContextualActionModeOverflowMenu(); 实体键盘菜单键

还有几个registerIdlingResourcesunregisterIdlingResources等关于IdlingResources的函数。以及本文的关键onView()onData(),在这里分别生成了ViewInteractionDataInteraction

 public static ViewInteraction onView(final Matcher viewMatcher) {
    return BASE.plus(new ViewInteractionModule(viewMatcher)).viewInteraction();
  }

注:onView 在这里使用了 Dagger 框架 ,由于笔者没有使用过该框架,在这里就不多赘述,感兴趣的可以自行查阅。

   public static DataInteraction onData(Matcher dataMatcher) {
    return new DataInteraction(dataMatcher);
  }

ViewInteraction

ViewInteraction中的公共函数非常少,只有四个:

函数名 功能
perform() 执行ViewAction操作
check() 检查ViewAssertion
inRoot() 确定目标view所在的root
withFailureHandler() 提供错误处理方式

1.perform()

我们还是按照三部曲的顺序进行先看perform()

public ViewInteraction perform(final ViewAction... viewActions) {
    checkNotNull(viewActions);
    for (ViewAction action : viewActions) {
        doPerform(action);
    }
    return this;
  }

从方法的形参ViewAction... viewActions我们可以知道perform()是支持同时执行多个操作的,但是会通过doPerform(action)按照顺序依次执行。
到这里问题就来了,如果按照三部曲的理解来说,现在应该开始对控件执行操作了,但是需要操作的控件在哪?我们至今没有看到,难道在onView()初始化的过程中已经将View检索出来存储为成员变量了?ok,我们来看一下 ViewInteraction 有哪些成员变量:

 //用于进行简单UI操作的工具
  private final UiController uiController;
  
 //用于查找View的工具
  private final ViewFinder viewFinder;
  
  //执行已提交 runnable 任务的对象
  private final Executor mainThreadExecutor;
  
  //错误处理机制与 withFailureHandler() 有关
  private volatile FailureHandler failureHandler;
  
  //view的匹配器(我们在onView(viewMatcher)传入的)
  private final Matcher viewMatcher;
  
  //缺点查询 view 的 root 与 inRoot() 有关
  private final AtomicReference> rootMatcherRef;

好吧,现实并不是想象的那样,ViewInteraction 并没有存储 view ,里面只有用于查找 view 的工具(ViewFinder)和材料(Matcher)。看来答案需要在接下来的doPerform(action)中寻找了。让我们看一下代码:

private void doPerform(final ViewAction viewAction) {
    checkNotNull(viewAction);
    final Matcher constraints = checkNotNull(viewAction.getConstraints());
    runSynchronouslyOnUiThread(new Runnable() {

      @Override
      public void run() {
        uiController.loopMainThreadUntilIdle();
        View targetView = viewFinder.getView();
        Log.i(TAG, String.format(
            "Performing '%s' action on view %s", viewAction.getDescription(), viewMatcher));
        if (!constraints.matches(targetView)) {
          // TODO(user): update this to describeMismatch once hamcrest is updated to new
          StringDescription stringDescription = new StringDescription(new StringBuilder(
              "Action will not be performed because the target view "
              + "does not match one or more of the following constraints:\n"));
          constraints.describeTo(stringDescription);
          stringDescription.appendText("\nTarget view: ")
              .appendValue(HumanReadables.describe(targetView));

          if (viewAction instanceof ScrollToAction
              && isDescendantOfA(isAssignableFrom((AdapterView.class))).matches(targetView)) {
            stringDescription.appendText(
                "\nFurther Info: ScrollToAction on a view inside an AdapterView will not work. "
                + "Use Espresso.onData to load the view.");
          }

          throw new PerformException.Builder()
            .withActionDescription(viewAction.getDescription())
            .withViewDescription(viewMatcher.toString())
            .withCause(new RuntimeException(stringDescription.toString()))
            .build();
        } else {
          viewAction.perform(uiController, targetView);
        }
      }
    });
  }

函数开始,先对viewAction的可看性进行检查,并获取viewAction操作对 view 限制条件 constraints (绝大多数操作只能在相对应的控件上进行操作),然后的操作在runSynchronouslyOnUiThread()中进行。
View targetView = viewFinder.getView();从字面上看应该是用来锁定目标 view 。接下来进行判断目标view是否符合constraints 要求,符合要求则正式进行perform(),不符合要求的话,会打印日志提示,当前空间无法执行本操作,并且判断是否是由于onView()&onData()的错误使用造成。


perform()的使用基本介绍完了。现在,我们找出我们想要的View targetView = viewFinder.getView();跟踪进去,看一下 view 是怎么来的:

public View getView() throws AmbiguousViewMatcherException, NoMatchingViewException {
    checkMainThread();
    final Predicate matcherPredicate = new MatcherPredicateAdapter(
        checkNotNull(viewMatcher));

    View root = rootViewProvider.get();
    Iterator matchedViewIterator = Iterables.filter(
        breadthFirstViewTraversal(root),
        matcherPredicate).iterator();

    View matchedView = null;

    while (matchedViewIterator.hasNext()) {
      if (matchedView != null) {
        // Ambiguous!
        throw new AmbiguousViewMatcherException.Builder()
            .withViewMatcher(viewMatcher)
            .withRootView(root)
            .withView1(matchedView)
            .withView2(matchedViewIterator.next())
            .withOtherAmbiguousViews(Iterators.toArray(matchedViewIterator, View.class))
            .build();
      } else {
        matchedView = matchedViewIterator.next();
      }
    }
    if (null == matchedView) {
      final Predicate adapterViewPredicate = new MatcherPredicateAdapter(
          ViewMatchers.isAssignableFrom(AdapterView.class));
      List adapterViews = Lists.newArrayList(
          Iterables.filter(breadthFirstViewTraversal(root), adapterViewPredicate).iterator());
      if (adapterViews.isEmpty()) {
        throw new NoMatchingViewException.Builder()
            .withViewMatcher(viewMatcher)
            .withRootView(root)
            .build();
      }
      String warning = String.format("\nIf the target view is not part of the view hierarchy, you "
        + "may need to use Espresso.onData to load it from one of the following AdapterViews:%s"
        , Joiner.on("\n- ").join(adapterViews));
      throw new NoMatchingViewException.Builder()
          .withViewMatcher(viewMatcher)
          .withRootView(root)
          .withAdapterViews(adapterViews)
          .withAdapterViewWarning(Optional.of(warning))
          .build();
    } else {
      return matchedView;
    }
  }

首先使用 inRoot() 设置的条件rootViewProvider获取需要的根布局,如果没有使用inRoot() ,会获取默认根布局。
View root = rootViewProvider.get();
将根布局使用breadthFirstViewTraversal()打算成单个的view
如图的布局,打散后为 Root, A, R, U, B, D, G, N

view

ViewsMatcher 匹配后的匹配结果,会存储在matchedViewIterator
Iterator matchedViewIterator = Iterables.filter(breadthFirstViewTraversal(root),matcherPredicate).iterator();
正常情况下的匹配结果matchedViewIterator 的 size 应该为 1,这样才符合onView()匹配结果有且仅有一个的特点,否则抛出有多个匹配结果的异常AmbiguousViewMatcherException:

  • Multiple Ambiguous Views found for matcher
  • matches multiple views in the hierarchy

如果没有任何匹配结果,则会抛出异常NoMatchingViewException

  • No views in hierarchy found matching

并判断是否可能由于错用onData()&onView导致(也就是当前布局是否存在AdapterView),额外抛出提示:

  • If the target view is not part of the view hierarchy, you may need to use Espresso.onData to load it from one of the following AdapterViews

如果以上异常全没有出现,那么恭喜了,我们期盼了许久的唯一的 view 可以从matchedViewIterator中取出进行操作了。


由于在perform()中使用 for() 循环,依次执行每一个 ViewAction ,且 view 是在每次执行中单独匹配,所以如果你在perform()中执行多个操作,请注意一下每个操作都是完全独立的,不要写出下面这种代码,不要问我为什么知道的 T-T

onView(withText("string1"))
                .perform(replaceText("string2"),closeSoftKeyboard());

这里onView()的 Matcher 为withText("string1")也就是带有 text 为 "string1" 的控件,然后replaceText("string2")将控件中的 "string1" 修改为 "string2"。一直到这里都没有问题,但是当开始在perform()中执行closeSoftKeyboard())时电脑就一脸懵逼了,你他喵的让我去找 "string1" (这里还是执行onView()中的 matcher),但是你开始前给我换成 "string2" 是几个意思。就像你拿着萌妹的照片去见网友,到了地方发现只有一群壮汉一样,节哀。

2.check()

看完perform()之后再看check()就会感觉异常的简单:

  public ViewInteraction check(final ViewAssertion viewAssert) {
    checkNotNull(viewAssert);
    runSynchronouslyOnUiThread(new Runnable() {
      @Override
      public void run() {
        uiController.loopMainThreadUntilIdle();

        View targetView = null;
        NoMatchingViewException missingViewException = null;
        try {
          targetView = viewFinder.getView();
        } catch (NoMatchingViewException nsve) {
          missingViewException = nsve;
        }
        viewAssert.check(targetView, missingViewException);
      }
    });
    return this;
  }

还是检查开始checkNotNull(viewAssert);然后在runSynchronouslyOnUiThread中开始执行操作。接下来的代码就非常干脆了直接targetView = viewFinder.getView();,找到就开始执行check(),找不到就抛出NoMatchingViewException

不知道是否注意到了无论check()还是perform()都会在runSynchronouslyOnUiThread的开始执行代码uiController.loopMainThreadUntilIdle();,这里就是等待主线程空闲的操作,这里就不深入去看了。

3.inRoot()

现在来看一下辅助函数inRoot(),这个函数非常简单:

  /**
   * Makes this ViewInteraction scoped to the root selected by the given root matcher.
   */
  public ViewInteraction inRoot(Matcher rootMatcher) {
    this.rootMatcherRef.set(checkNotNull(rootMatcher));
    return this;
  }

将当前的rootMatcher存储到AtomicReference> rootMatcherRef中(注:使用原子性对象引用,在多线程情况下进行对象的更新可以确保一致性,暂时理解为一种存储形式吧)
这个函数简单的我一脸的黑人问号,不过没关系,我们继续挖。
首先我们看一下rootMatcherRef在哪里用过,在 Espresso类中找是不现实了,因为这里只有定义,根本没有任何使用的迹象。不过在我们寻找 view 的时候ewFinder.getView();的最开始有一段用来确认 root 的代码:

View root = rootViewProvider.get();

我们打开 RootViewProvider 类,走!看代码!

private final AtomicReference> rootMatcherRef;

在 RootViewProvider 中有 rootMatcherRef 同样的原子性引用,这里是不是和我们想要看的 inRoot() 有关系呢。关系到引用是不是同一个引用的问题,我们就只能到最开始的构造去找了。
再次打开onView(),跟踪到new ViewInteractionModule(viewMatcher),看一下他的成员函数

private final AtomicReference> rootMatcher =

      new AtomicReference>(RootMatchers.DEFAULT);

所有的源头找到了,然后这里构造的AtomicReferenceDaggerBaseLayerComponent 类的内部类 ViewInteractionComponentImpl 类 中初始化到 ViewInteraction 的各个成员中。
看到这里inRoot()的作用和运行过程,相信你应该有个大概的印象了,他正式产生作用就是在我们最开始看到的

View root = rootViewProvider.get();

在这里使用和查找 view 类似的方式获取了当前视图的所有 root 并筛选出有且仅有一个的结果:

FindRootResult findResult = findRoot(rootMatcher);

然后确认当前的 root is ready,并返回root对应的decorView,用于参与后续操作(提取布局内所有view)

//检查当前root状态是否可用
isReady(findResult.needle)
//返回root对应的decorView
findResult.needle.getDecorView()

4.withFailureHandler()

设置对于抛出异常的处理方式,处理方式需要实现接口 FailureHandler,里面只有一个函数handle()用来完成异常处理。
这里看一下默认的异常处理方式

public void handle(Throwable error, Matcher viewMatcher) {
    if (error instanceof EspressoException || error instanceof AssertionFailedError
        || error instanceof AssertionError) {
      throw propagate(getUserFriendlyError(error, viewMatcher));
    } else {
      throw propagate(error);
    }
  }

函数对Espresso的异常常规异常分别处理打印日志,但是在真正使用中这样的日志效果并不是非常的好,所以可以根据情况自定义 FailureHandler,增加更详细的日志,或者增加截屏功能等。

DataInteraction

DataInteraction的公共函数有七个,除去perform()check()之外,有五个辅助函数:

函数名 功能
atPosition() 选中匹配的Adapter的第几项(出现重复匹配时使用)
inAdapterView 选择满足adapterMatcher的adapterview来执行onData操作
inRoot 锁定指定的root
onChildView 匹配”onData所匹配的item视图”中的指定子视图
usingAdapterViewProtocol 对于不符合常规的AdapterView自定义协议

下面依次来看一下

1.perform()

本来感觉和onData()onView()时完全并列的两条主线,直到看了onData()的代码,发现,原来他是个弟弟:

  public ViewInteraction perform(ViewAction... actions) {

       AdapterDataLoaderAction adapterDataLoaderAction = load();

       return onView(makeTargetMatcher(adapterDataLoaderAction))
            .inRoot(rootMatcher)
            .perform(actions);

  }

这里把他整体分为两个部分吧第一部分load()先略过去,我们先看第二部分,直接将onView搬过来了。inRoot()perform()在上文说的够多了,这里主要看makeTargetMatcher ()

private Matcher makeTargetMatcher(AdapterDataLoaderAction adapterDataLoaderAction) 
{
        Matcher targetView = displayingData(adapterMatcher, dataMatcher, 
adapterViewProtocol,
        adapterDataLoaderAction);
    if (childViewMatcher.isPresent()) {
        targetView = allOf(childViewMatcher.get(), isDescendantOfA(targetView));
    }
    return targetView;
  }

这里没有太特别的操作,只是区分了一下onChildView()带来的影响。具体在onChildView()中会进行分析。
我们看一下生成匹配器的函数displayingData(),内部和常规的 TypeSafeMatcher没有什么区别,这里我们主要看matchesSafely()函数:
()

public boolean matchesSafely(View view) {
        ViewParent parent = view.getParent();
        while (parent != null && !(parent instanceof AdapterView)) {
          parent = parent.getParent();
        }
        if (parent != null && adapterMatcher.matches(parent)) {
          Optional data = adapterViewProtocol.getDataRenderedByView(
              (AdapterView) parent, view);
          if (data.isPresent()) {
            return adapterDataLoaderAction.getAdaptedData().opaqueToken.equals(
                data.get().opaqueToken);
          }
        }
        return false;
      }

开始检测view 的ViewParent 是否为 AdapterView ,如果不是直接抬走下一位,如果是则通过getDataRenderedByView()获取当前view对应的 data ,并将它与 objectMatcher 匹配的结果进行比较(objectMatcher在 load()中进行)。
可能你还没有了解自定义matcher ,这里解释一下 ,形参view就是上文提到的getView()函数中breadthFirstViewTraversal(root)的结果(排序后的视图内所有view),依次输入匹配。
看到这里perform()的工作流程就可以明白一二了:因为onView()是用来定位视图内的控件,所以第一步肯定是将目标视图移动到视图内,然后才能进行第二步的view选择和操作。
现在看一下第一步中,是如何进行view的移动的。
AdapterDataLoaderAction adapterDataLoaderAction = load();
现在看一下load()函数:

  private AdapterDataLoaderAction load() {
    AdapterDataLoaderAction adapterDataLoaderAction =
       new AdapterDataLoaderAction(dataMatcher, atPosition, adapterViewProtocol);
    onView(adapterMatcher)
      .inRoot(rootMatcher)
      .perform(adapterDataLoaderAction);
    return adapterDataLoaderAction;
  }

这里用来将 目标view 移动到视图内用的还是onView(),注意看,这里匹配的控件是 AdapterView,因为AdapterView 一定是在视图范围内的,而且,view 是 AdapterView的子视图,所以这里用onView()对AdapterView进行操作没有问题。
我们看一下这里进行了什么操作。由于AdapterDataLoaderAction implements ViewAction所以这里我们直接看perform()函数:

  public void perform(UiController uiController, View view) {
  //第一部分
    AdapterView adapterView = (AdapterView) view;
    List matchedDataItems = Lists.newArrayList();
    for (AdapterViewProtocol.AdaptedData data : adapterViewProtocol.getDataInAdapterView(
        adapterView)) {
      if (dataToLoadMatcher.matches(data.getData())) {
        matchedDataItems.add(data);
      }
    }
    //第二部分
    if (matchedDataItems.size() == 0) {
      StringDescription dataMatcherDescription = new StringDescription();
      dataToLoadMatcher.describeTo(dataMatcherDescription);
      if (matchedDataItems.isEmpty()) {
        dataMatcherDescription.appendText(" contained values: ");
          dataMatcherDescription.appendValue(
              adapterViewProtocol.getDataInAdapterView(adapterView));
        throw new PerformException.Builder()
          .withActionDescription(this.getDescription())
          .withViewDescription(HumanReadables.describe(view))
          .withCause(new RuntimeException("No data found matching: " + dataMatcherDescription))
          .build();
      }
    }
    //第三部分
    synchronized (dataLock) {
      checkState(!performed, "perform called 2x!");
      performed = true;
      if (atPosition.isPresent()) {
        int matchedDataItemsSize = matchedDataItems.size() - 1;
        if (atPosition.get() > matchedDataItemsSize) {
          throw new PerformException.Builder()
            .withActionDescription(this.getDescription())
            .withViewDescription(HumanReadables.describe(view))
            .withCause(new RuntimeException(String.format(
                "There are only %d elements that matched but requested %d element.",
                matchedDataItemsSize, atPosition.get())))
            .build();
        } else {
          adaptedData = matchedDataItems.get(atPosition.get());
        }
      } else {
        if (matchedDataItems.size() != 1) {
          StringDescription dataMatcherDescription = new StringDescription();
          dataToLoadMatcher.describeTo(dataMatcherDescription);
          throw new PerformException.Builder()
            .withActionDescription(this.getDescription())
            .withViewDescription(HumanReadables.describe(view))
            .withCause(new RuntimeException("Multiple data elements " +
                "matched: " + dataMatcherDescription + ". Elements: " + matchedDataItems))
            .build();
        } else {
          adaptedData = matchedDataItems.get(0);
        }
      }
    }
    //第四部分
    int requestCount = 0;
    while (!adapterViewProtocol.isDataRenderedWithinAdapterView(adapterView, adaptedData)) {
      if (requestCount > 1) {
        if ((requestCount % 50) == 0) {
          // sometimes an adapter view will receive an event that will block its attempts to scroll.
          adapterView.invalidate();
          adapterViewProtocol.makeDataRenderedWithinAdapterView(adapterView, adaptedData);
        }
      } else {
        adapterViewProtocol.makeDataRenderedWithinAdapterView(adapterView, adaptedData);
      }
      uiController.loopMainThreadForAtLeast(100);
      requestCount++;
    }
  }

由于代码比较比较长,这里分为几部分来看:
第一部分:view 强制转换为 AdapterView ,取出 data 与dataToLoadMatcher()进行匹配,将所有匹配成功的结果 存储到 matchedDataItems中。
第二部分:如果matchedDataItems为空,及没有任何匹配数据,则抛出异常。
第三部分:这里会根据是否使用了atPosition()产生区别。如果使用了则会返回matchedDataItems.get(atPosition.get())类似于 List().get(atPosition),和常规使用List一样,这里会判断是否“指针超限”。如果没有使用,就需要看matchedDataItems.size()如果正好为 0 ,可以直接返回结果,否则就会抛出Multiple data elements的异常。
第四部分:这里就是将 目标view 滑动到视图内的操作。这里有注释

// sometimes an adapter view will receive an event that will block its attempts to scroll.

这里不会无限制的进行尝试操作,如果超过限制会放弃本次操作。当然这不是我们想看到的,我们继续看代码makeDataRenderedWithinAdapterView()(这里就只贴关键代码了)

//第一部分
((AbsListView) adapterView).smoothScrollToPositionFromTop(position,
                adapterView.getPaddingTop(), 0);
                
    ......
                
 //第二部分  
if (adapterView instanceof AdapterViewAnimator) {
    if (adapterView instanceof AdapterViewFlipper) {
        ((AdapterViewFlipper) adapterView).stopFlipping();
    }
        ((AdapterViewAnimator) adapterView).setDisplayedChild(position);
        moved = true;
}

第一部分就是滑动到指定位置的主体函数,第二部分是关于动画处理的操作,这里就不介绍了(笔者还没有动画相关内容)。现在着重看第一部分。
跟踪进去,主要代码只有一句:

 AbsPositionScroller.startWithOffset();

继续跟踪到 AbsListView().AbsPositionScroller 类,这里已经是ListView,就不贴代码了,在这里说一下流程:
首先在startWithOffset()中会做一些判定和预处理,并计算需要滑动的参数,然后postOnAnimation(this)(因为AbsPositionScroller implement Runnable)开始运行run()在这里进行不同情况的判断正式开始滑动操作。
到此perform()介绍完毕,说了这么多总结起来就两部:1、将 目标View 移动到视图中;2、调用 onView

2.check()

  public ViewInteraction check(ViewAssertion assertion) {
     AdapterDataLoaderAction adapterDataLoaderAction = load();
     return onView(makeTargetMatcher(adapterDataLoaderAction))
        .inRoot(rootMatcher)
        .check(assertion);
  }

直接贴出代码,相信你一定马上明白了,和perform整体操作完全一样,这里就不多加介绍了。

3.inRoot()

inRoot() 就是直接调用 ViewInteraction 看一下上面 check()中的 return 就明白了。

4.inAdapterView()

看一下它的使用吧,首先是在 上面提到的load()perform()&check()调用的第一个函数)中

    onView(adapterMatcher)
      .inRoot(rootMatcher)
      .perform(adapterDataLoaderAction);

直接放在onView()中用来匹配 adapterView ,再就是 displayingData()中用来作为匹配 view 的保障。(详细见上文perform())

public boolean matchesSafely(View view) {

    ......
    
    if (parent != null && adapterMatcher.matches(parent)) {
             Optional data =  adapterViewProtocol.getDataRenderedByView(
                 (AdapterView) parent, view);
             if (data.isPresent()) {
                    return adapterDataLoaderAction.getAdaptedData().opaqueToken.equals(
                              data.get().opaqueToken);
          }
    }
}

4.atPosition()

AdapterDataLoaderAction.perform()的第三部分中(详见上文):

adaptedData = matchedDataItems.get(atPosition.get());

如果使用dataMatcher匹配的结果多于一个,则需要atPosition来进行甄别,确定唯一结果,否则报错。(可以在onData()中不筛选任何data,添加空dataMatcher,直接用atPosition(),当然,这种方法不推荐)

5.usingAdapterViewProtocol()

本函数中涉及的AdapterViewProtocol是整个onData的中心,本文中使用的makeDataRenderedWithinAdapterView()
AdapterDataLoaderAction.perform()
等函数都直接或者间接的使用了默认的AdapterViewProtocol,它是Espresso访问AdapterView的中间桥梁。
常规情况下,默认的AdapterViewProtocol可以满足使用情况,不需要使用,如果您想了解推荐你看一下Espresso的进阶: AdapterViewProtocol,大神写的非常详细。

总结

本文是笔者按照自己看代码的思路写的,整体可能会比较繁琐,也会有思考不到位的地方,请见谅,这里是对于上文提到的一些关键点的总结:

  1. onData()&onView只是起到初始化的作用,真正的定位view操作实际在perform()&check中执行。
  2. 不要把perform()&check定位view想的太复杂,就是将所有view排序后一个一个进行匹配尝试。所以针对线型布局中这种顺序定死的view,也可以自定义matcher排号选择第几个。
  3. 每个perform()&check()都是单独进行 定位view 的操作,后续操作必须考虑上一个操作带来的影响。
  4. 同一个perform()执行多个操作,和连续使用多个perform()的效果是相同的,必须注意操作的前后影响
  5. 使用withFailureHandler()自定义新的FailureHandler,根据需求增加log屏幕截图,单纯的原始log无法满足需求。
  6. 对于 AdapterView 推荐使用 onData() ,哪怕是视图可见的 view 。不然当AdapterView的数据源发生变化时,你就可以去哭了。
  7. onData() 是为了适应AdapterView 而封装后的 onView()
函数名 功能
atPosition() 选中匹配的Adapter的第几项(出现重复匹配时使用)
inAdapterView 选择满足adapterMatcher的adapterview来执行onData操作
inRoot 锁定指定的root
onChildView 匹配”onData所匹配的item视图”中的指定子视图
usingAdapterViewProtocol 对于不符合常规的AdapterView自定义协议

最后,不要盲目的使用录制软件,录制后的代码需要检查一遍有没有“坑”。

你可能感兴趣的:(Espresso之onView()&onData())