Espresso-core庫分析

Google為他們寫的Espresso框架也寫單元測試/集成測試代碼盾沫,讓我們先從這些測試代碼出發(fā)看一下Espresso框架的使用沸毁,讓我們從EspressoTest這個測試類看起吧,這個類的源碼在android-support-test/frameworks/testing/espresso/core-tests/src/androidTest/java/android/support/test/espresso/EspressoTest.java亥鬓,目標(biāo)測試工程是一個叫android.support.test.testapp的工程完沪,位于android-support-test/frameworks/testing/espresso/sample路徑,Google書寫了許多不同類型的Activity用于測試Espresso嵌戈,大家也可以自行查看這個目標(biāo)工程的源碼覆积。

EspressoTest中有若干測試方法,我們隨便選取一個測試方法在這里展示出來分析給大家熟呛,因此下面的代碼僅是EspressoTest的一部分:

public class EspressoTest extends ActivityInstrumentationTestCase2<MainActivity> {
  @SuppressWarnings("deprecation")
  public EspressoTest() {
    // Supporting froyo.
    super("android.support.test.testapp", MainActivity.class);
  }

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

  @SuppressWarnings("unchecked")
  public void testOpenOverflowFromActionBar() {
    onData(allOf(instanceOf(Map.class), hasValue(ActionBarTestActivity.class.getSimpleName())))
        .perform(click());
    onView(withId(R.id.hide_contextual_action_bar))
        .perform(click());
    openActionBarOverflowOrOptionsMenu(getInstrumentation().getTargetContext());
    onView(withText("World"))
        .perform(click());
    onView(withId(R.id.text_action_bar_result))
        .check(matches(withText("World")));
  }
}

可以看到宽档,Espresso的測試工程也是繼承了ActivityInstrumentationTestCase2這個我們十分熟悉的測試類,故Espresso框架下的測試用例也是基于Instrumentation框架的庵朝,之前我們對Instrumentation框架的分析也都適用與它吗冤。從結(jié)構(gòu)上看這個測試類是基于JUnit3框架的,測試方法中頻繁出現(xiàn)了onView和onData的方法九府,讓我們來跟蹤一下椎瘟,看看他們是做什么用的。

從onView方法看起學(xué)習(xí)依賴注入

根據(jù)onView方法侄旬,發(fā)現(xiàn)是實(shí)現(xiàn)位于Espresso.java類

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

其返回為一個ViewInteraction對象肺蔚,需要一個類型為Matcher<View>的參數(shù),調(diào)用了BASE.plus方法儡羔,BASE是一個BaseLayerComponent類型的對象

private static final BaseLayerComponent BASE = GraphHolder.baseLayer();

再看GraphHolder類:

/**
 * Holds Espresso's object graph.
 */
public final class GraphHolder {

  private static final AtomicReference<GraphHolder> instance =
      new AtomicReference<GraphHolder>(null);

  private final BaseLayerComponent component;

  private GraphHolder(BaseLayerComponent component) {
    this.component = checkNotNull(component);
  }

  static BaseLayerComponent baseLayer() {
    GraphHolder instanceRef = instance.get();
    if (null == instanceRef) {
      instanceRef = new GraphHolder(DaggerBaseLayerComponent.create());
      if (instance.compareAndSet(null, instanceRef)) {
        UsageTrackerRegistry.getInstance().trackUsage("Espresso");
        return instanceRef.component;
      } else {
        return instance.get().component;
      }
    } else {
      return instanceRef.component;
    }
  }
}

看上去我們需要的這個BaseLayerComponent的實(shí)例應(yīng)該是由DaggerBaseLayerComponent.create()方法生成的婆排,可是找遍所有的源碼也沒有找到有一個類的名字叫DaggerBaseLayerComponent,而且之前也看到了Onview方法會調(diào)用這個BaseLayerComponent類實(shí)例的plus方法笔链,BaseLayerComponent是一個接口段只,同時也沒有找到有這個接口的實(shí)現(xiàn)類,自然也沒有找到plus方法的實(shí)現(xiàn)了鉴扫,BaseLayerComponent類源碼如下:

/**
 * Dagger component for base classes.
 */
@Component(modules = {BaseLayerModule.class, UiControllerModule.class})
@Singleton
public interface BaseLayerComponent {
  BaseLayerModule.FailureHandlerHolder failureHolder();
  FailureHandler failureHandler();
  ActiveRootLister activeRootLister();
  IdlingResourceRegistry idlingResourceRegistry();
  ViewInteractionComponent plus(ViewInteractionModule module);
}

明明編譯都是正常的赞枕,難道這些類的實(shí)例和接口方法的實(shí)現(xiàn)就這樣憑空消失了?肯定不會,這時候就是依賴注入框架Dagger2需要出來發(fā)威的時候了炕婶。依賴注入的介紹和Dagger2框架內(nèi)容比較多姐赡,我單獨(dú)開了一個頁面來介紹他們,大家可以移步:依賴注入及Dagger2框架的介紹

Dagger2在Espresso源碼中的應(yīng)用

通過對Dagger2框架的學(xué)習(xí)我們知道了GraphHolder類的baseLayer()方法返回的BaseLayerComponent對象是Dagger2框架通過DaggerBaseLayerComponent.create()方法創(chuàng)建的實(shí)例柠掂,在回去看onView方法:

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

plus方法是BaseLayerComponent中ViewInteractionComponent類型的注入项滑,需要一個ViewInteractionModule類型的依賴,而ViewInteractionComponent又是BaseLayerComponent的一個Subcomponent:

/**
 * Dagger component for view interaction classes.
 */
@Subcomponent(modules = ViewInteractionModule.class)
@Singleton
public interface ViewInteractionComponent {
  ViewInteraction viewInteraction();
}

提供的viewInteraction正好是onView中調(diào)用的涯贞,所以整個onView方法返回的是一個ViewInteraction類型注入的實(shí)例枪狂,查看ViewInteraction的源碼,我們先看構(gòu)造函數(shù)的注入部分:

  @Inject
  ViewInteraction(
      UiController uiController,
      ViewFinder viewFinder,
      @MainThread Executor mainThreadExecutor,
      FailureHandler failureHandler,
      Matcher<View> viewMatcher,
      AtomicReference<Matcher<Root>> rootMatcherRef) {
    this.viewFinder = checkNotNull(viewFinder);
    this.uiController = checkNotNull(uiController);
    this.failureHandler = checkNotNull(failureHandler);
    this.mainThreadExecutor = checkNotNull(mainThreadExecutor);
    this.viewMatcher = checkNotNull(viewMatcher);
    this.rootMatcherRef = checkNotNull(rootMatcherRef);
  }

可以看到這個注入是依賴與6個參數(shù)的宋渔,類型分別為UiController州疾,ViewFinder,Executor皇拣,F(xiàn)ailureHandler严蓖,Matcher<View>,AtomicReference<Matcher<Root>>氧急,這些依賴均是由BaseLayerComponent和ViewInteractionComponent聲明的Modules們(BaseLayerModule颗胡,UiControllerModule,ViewInteractionModule)提供的吩坝,我就直接節(jié)選這些Module中的實(shí)現(xiàn)給大家看了:

BaseLayerModule.java:

  @Provides
  FailureHandler provideFailureHandler(FailureHandlerHolder holder) {
    return holder.get();
  }

  @Provides
  @Default
  FailureHandler provideFailureHander() {
    return new DefaultFailureHandler(InstrumentationRegistry.getTargetContext());
  }
  
  @Provides @Singleton @MainThread
  public Executor provideMainThreadExecutor(Looper mainLooper) {
    final Handler handler = new Handler(mainLooper);
    return new Executor() {
      @Override
      public void execute(Runnable runnable) {
        handler.post(runnable);
      }
    };
  }

UiControllerModule.java:

  @Provides
  public UiController provideUiController(UiControllerImpl uiControllerImpl) {
    return uiControllerImpl;
  }

ViewInteractionModule.java:

  @Provides
  AtomicReference<Matcher<Root>> provideRootMatcher() {
    return rootMatcher;
  }

  @Provides
  Matcher<View> provideViewMatcher() {
    return viewMatcher;
  }

  @Provides
  ViewFinder provideViewFinder(ViewFinderImpl impl) {
    return impl;
  }

可以看到這些依賴項(xiàng)又有自己的依賴項(xiàng)毒姨,我們可以先不用急著把他們的關(guān)系理的清清楚楚,可以在主流程中慢慢的一一弄清钾恢。

回到onView方法學(xué)習(xí)框架設(shè)計思路

回到之前Espresso自帶的測試類中的onView方法吧:

onView(withId(R.id.hide_contextual_action_bar)).perform(click());

onview方法實(shí)際返回的是ViewInteractionComponent中viewInteraction()方法的依賴手素,即一個ViewInteraction對象鸳址,傳入的參數(shù)withId(R.id.hide_contextual_action_bar)是一個Matcher<View> viewMatcher類型的參數(shù)瘩蚪,有經(jīng)驗(yàn)的同學(xué)應(yīng)該能知道這是一個基于View類型的匹配器,然后執(zhí)行了ViewInteraction對象的perform(click())方法稿黍,看一下perform方法的實(shí)現(xiàn):

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

很好理解疹瘦,perform方法可以傳入若干個ViewAction對象,然后會依次對這些ViewAction對象執(zhí)行doPerform方法巡球,doPerform方法的實(shí)現(xiàn):

private void doPerform(final ViewAction viewAction) {
    checkNotNull(viewAction);
    final Matcher<? extends View> 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);
        }
      }
    });
  }

這段代碼主要是在主線程中插入了一段方法執(zhí)行言沐,而這段方法中有幾個關(guān)鍵方法:

  • uiController.loopMainThreadUntilIdle();
  • View targetView = viewFinder.getView();
  • viewAction.perform(uiController, targetView);
    下面我們就來看看這3行代碼分別做了什么事情:

uiController.loopMainThreadUntilIdle()

UiControllerImpl是UiController的實(shí)現(xiàn),先過一下構(gòu)造函數(shù)酣栈,當(dāng)然這個實(shí)例也會通過Dagger2框架自動實(shí)例化:

  @VisibleForTesting
  @Inject
  UiControllerImpl(EventInjector eventInjector,
      @SdkAsyncTask AsyncTaskPoolMonitor asyncTaskMonitor,
      @CompatAsyncTask @Nullable AsyncTaskPoolMonitor compatTaskMonitor,
      IdlingResourceRegistry registry,
      Looper mainLooper,
      Recycler recycler) {
    this.eventInjector = checkNotNull(eventInjector);
    this.asyncTaskMonitor = checkNotNull(asyncTaskMonitor);
    this.compatTaskMonitor = compatTaskMonitor;
    this.conditionSet = IdleCondition.createConditionSet();
    this.idlingResourceRegistry = checkNotNull(registry);
    this.mainLooper = checkNotNull(mainLooper);
    this.queueInterrogator = new QueueInterrogator(mainLooper);
    this.recycler = checkNotNull(recycler);
  }

我們暫時不急于去找到這些依賴的來源险胰,先直接看一下我們要分析的loopMainThreadUntilIdle方法:

  public void loopMainThreadUntilIdle() {
    initialize();
    checkState(Looper.myLooper() == mainLooper, "Expecting to be on main thread!");
    do {
      EnumSet<IdleCondition> condChecks = EnumSet.noneOf(IdleCondition.class);
      if (!asyncTaskMonitor.isIdleNow()) {
        asyncTaskMonitor.notifyWhenIdle(new SignalingTask<Void>(NO_OP,
            IdleCondition.ASYNC_TASKS_HAVE_IDLED, generation));

        condChecks.add(IdleCondition.ASYNC_TASKS_HAVE_IDLED);
      }

      if (!compatIdle()) {
        compatTaskMonitor.notifyWhenIdle(new SignalingTask<Void>(NO_OP,
            IdleCondition.COMPAT_TASKS_HAVE_IDLED, generation));
        condChecks.add(IdleCondition.COMPAT_TASKS_HAVE_IDLED);
      }

      if (!idlingResourceRegistry.allResourcesAreIdle()) {
        final IdlingPolicy warning = IdlingPolicies.getDynamicIdlingResourceWarningPolicy();
        final IdlingPolicy error = IdlingPolicies.getDynamicIdlingResourceErrorPolicy();
        final SignalingTask<Void> idleSignal = new SignalingTask<Void>(NO_OP,
            IdleCondition.DYNAMIC_TASKS_HAVE_IDLED, generation);
        idlingResourceRegistry.notifyWhenAllResourcesAreIdle(new IdleNotificationCallback() {
          @Override
          public void resourcesStillBusyWarning(List<String> busyResourceNames) {
            warning.handleTimeout(busyResourceNames, "IdlingResources are still busy!");
          }

          @Override
          public void resourcesHaveTimedOut(List<String> busyResourceNames) {
            error.handleTimeout(busyResourceNames, "IdlingResources have timed out!");
            controllerHandler.post(idleSignal);
          }

          @Override
          public void allResourcesIdle() {
            controllerHandler.post(idleSignal);
          }
        });
        condChecks.add(IdleCondition.DYNAMIC_TASKS_HAVE_IDLED);
      }

      try {
        loopUntil(condChecks);
      } finally {
        asyncTaskMonitor.cancelIdleMonitor();
        if (null != compatTaskMonitor) {
          compatTaskMonitor.cancelIdleMonitor();
        }
        idlingResourceRegistry.cancelIdleMonitor();
      }
    } while (!asyncTaskMonitor.isIdleNow() || !compatIdle()
        || !idlingResourceRegistry.allResourcesAreIdle());

  }

從命名上看,該函數(shù)的作用是循環(huán)等待直到主線程空閑矿筝,共有三個條件:

  • asyncTaskMonitor.isIdleNow()
  • compatIdle()
  • idlingResourceRegistry.allResourcesAreIdle()

其中asyncTaskMonitor和compatTaskMonitor都是AsyncTaskPoolMonitor類型的依賴對象實(shí)例起便,通過不同的注解區(qū)分,他們分別對應(yīng)了BaseLayerModule中如下兩段方法實(shí)現(xiàn):

  @Provides @Singleton @CompatAsyncTask @Nullable
  public AsyncTaskPoolMonitor provideCompatAsyncTaskMonitor(
      ThreadPoolExecutorExtractor extractor) {
    Optional<ThreadPoolExecutor> compatThreadPool = extractor.getCompatAsyncTaskThreadPool();
    if (compatThreadPool.isPresent()) {
      return new AsyncTaskPoolMonitor(compatThreadPool.get());
    } else {
      return null;
    }
  }
  
  @Provides @Singleton @SdkAsyncTask
  public AsyncTaskPoolMonitor provideSdkAsyncTaskMonitor(ThreadPoolExecutorExtractor extractor) {
    return new AsyncTaskPoolMonitor(extractor.getAsyncTaskThreadPool());

  }

他們對應(yīng)的參數(shù)依賴ThreadPoolExecutorExtractor的構(gòu)造方法:

  @Inject
  ThreadPoolExecutorExtractor(Looper looper) {
    mainHandler = new Handler(looper);
  }

Looper的依賴提供(位于BaseLayerModule中):

  @Provides @Singleton
  public Looper provideMainLooper() {
    return Looper.getMainLooper();
  }

看到這里有點(diǎn)開發(fā)經(jīng)驗(yàn)的同學(xué)都知道了是獲取主線程的Looper,然后回到ThreadPoolExecutorExtractor類查看getCompatAsyncTaskThreadPool方法和getAsyncTaskThreadPool方法:

  public Optional<ThreadPoolExecutor> getCompatAsyncTaskThreadPool() {
    try {
      return runOnMainThread(
          new FutureTask<Optional<ThreadPoolExecutor>>(MODERN_ASYNC_TASK_EXTRACTOR)).get();
    } catch (InterruptedException ie) {
      throw new RuntimeException("Interrupted while trying to get the compat async executor!", ie);
    } catch (ExecutionException ee) {
      throw new RuntimeException(ee.getCause());
    }
  }
  
  public ThreadPoolExecutor getAsyncTaskThreadPool() {
    FutureTask<Optional<ThreadPoolExecutor>> getTask = null;
    if (Build.VERSION.SDK_INT < 11) {
      getTask = new FutureTask<Optional<ThreadPoolExecutor>>(LEGACY_ASYNC_TASK_EXECUTOR);
    } else {
      getTask = new FutureTask<Optional<ThreadPoolExecutor>>(POST_HONEYCOMB_ASYNC_TASK_EXECUTOR);
    }

    try {
      return runOnMainThread(getTask).get().get();
    } catch (InterruptedException ie) {
      throw new RuntimeException("Interrupted while trying to get the async task executor!", ie);
    } catch (ExecutionException ee) {
      throw new RuntimeException(ee.getCause());
    }
  }

再具體的實(shí)現(xiàn)涉及到FutureTask相關(guān)邏輯榆综,有Android基礎(chǔ)的同學(xué)可以研究下妙痹,實(shí)際就是獲取各種同步任務(wù)的線程狀態(tài)。

idlingResourceRegistry是IdlingResourceRegistry類的實(shí)現(xiàn)鼻疮,以下是allResourcesAreIdle方法的源碼

  boolean allResourcesAreIdle() {
    checkState(Looper.myLooper() == looper);
    for (int i = idleState.nextSetBit(0); i >= 0 && i < resources.size();
        i = idleState.nextSetBit(i + 1)) {
      idleState.set(i, resources.get(i).isIdleNow());
    }
    return idleState.cardinality() == resources.size();
  }

其中idleState是一個BitSet對象怯伊,每一位Bit對應(yīng)的是當(dāng)前UI上每一個資源(View)是否為Idle狀態(tài)

viewFinder.getView()

ViewFinderImpl是ViewFinder的實(shí)現(xiàn),還是從構(gòu)造方法看起:

  @Inject
  ViewFinderImpl(Matcher<View> viewMatcher, Provider<View> rootViewProvider) {
    this.viewMatcher = viewMatcher;
    this.rootViewProvider = rootViewProvider;
  }

其中viewMatcher在ViewInteractionModule中定義判沟,實(shí)際就是onView方法傳入的Matcher<View>參數(shù)耿芹,RootViewPicker是Provider<View>的實(shí)現(xiàn)

下面是getView方法源碼:

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

    View root = rootViewProvider.get();
    Iterator<View> 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<View> adapterViewPredicate = new MatcherPredicateAdapter<View>(
          ViewMatchers.isAssignableFrom(AdapterView.class));
      List<View> 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;
    }
  }

首先使用viewMatcher構(gòu)造了一個Predicate<View>對象matcherPredicate,其中MatcherPredicateAdapter類源碼如下:

  private static class MatcherPredicateAdapter<T> implements Predicate<T> {
    private final Matcher<? super T> matcher;

    private MatcherPredicateAdapter(Matcher<? super T> matcher) {
      this.matcher = checkNotNull(matcher);
    }

    @Override
    public boolean apply(T input) {
      return matcher.matches(input);
    }
  }

matcher.matches是用于判斷對象是否滿足Matcher的條件的水评,因此matcherPredicate是用來斷言給定View是否能符合onView方法傳入Matcher<View>的條件的猩系。

之后根據(jù)rootViewProvider.get()獲取到當(dāng)前UI的根節(jié)點(diǎn),然后通過根節(jié)點(diǎn)遍歷所有的子節(jié)點(diǎn)中燥,尋找符合要求的View的迭代器(可能沒找到寇甸,找到1個或者找到多個匹配),僅在僅找到1個匹配時返回找到的這個View疗涉,否則報錯拿霉。

viewAction.perform(uiController, targetView)

這個部分就最簡單了,在前面的兩部確認(rèn)當(dāng)前主線程Idle咱扣,且找到了目標(biāo)View之后就是對目標(biāo)View執(zhí)行操作了绽淘,所有的操作都是ViewAction接口的實(shí)現(xiàn),通過實(shí)現(xiàn)ViewAction接口的perform方法,完成點(diǎn)擊抄沮,滑動旱易,拖拽手勢等操作下面以click操作為例看看ViewAction的行為是如何傳遞到手機(jī)APP上的。

從click方法看ViewAction的實(shí)現(xiàn)

先看click方法的源碼杀怠,位于ViewActions.java:

  public static ViewAction click() {
    return actionWithAssertions(
        new GeneralClickAction(Tap.SINGLE, GeneralLocation.VISIBLE_CENTER, Press.FINGER));
  }

actionWithAssertions的作用在它的注釋里寫的很清楚,在全部斷言通過后執(zhí)行給定的viewAction厅克,斷言集是globalAssertions參數(shù)中的赔退,有心的去看下源碼會發(fā)現(xiàn)一般情況下這個集都是空的,所以實(shí)際上actionWithAssertions會直接調(diào)用new GeneralClickAction(Tap.SINGLE, GeneralLocation.VISIBLE_CENTER, Press.FINGER)的perform方法

  /**
   * Performs all assertions before the {@code ViewAction}s in this class and then performs the
   * given {@code ViewAction}
   *
   * @param viewAction the {@code ViewAction} to perform after the assertions
   */
  public static ViewAction actionWithAssertions(final ViewAction viewAction) {
    if (globalAssertions.isEmpty()) {
      return viewAction;
    }
    
    ...
  }

先看下給GeneralClickAction傳入的參數(shù):

  • Tap.SINGLE(類型Tapper证舟,點(diǎn)擊動作):點(diǎn)擊動作單擊
  • GeneralLocation.CENTER(類型CoordinatesProvider硕旗,GeneralLocation是其實(shí)現(xiàn),點(diǎn)擊位置):點(diǎn)擊位置控件中央
  • Press.FINGER(類型PrecisionDescriber女责,觸控范圍):觸控范圍為手指漆枚,查看源碼可以看到FINGER的注釋為average width of the index finger is 16 – 20 mm.
  public GeneralClickAction(Tapper tapper, CoordinatesProvider coordinatesProvider,
      PrecisionDescriber precisionDescriber) {
    this(tapper, coordinatesProvider, precisionDescriber, null);
  }

再看下perform方法:

  public void perform(UiController uiController, View view) {
    float[] coordinates = coordinatesProvider.calculateCoordinates(view);
    float[] precision = precisionDescriber.describePrecision();

    Tapper.Status status = Tapper.Status.FAILURE;
    int loopCount = 0;
    
    while (status != Tapper.Status.SUCCESS && loopCount < 3) {
      try {
        status = tapper.sendTap(uiController, coordinates, precision);
      } catch (RuntimeException re) {
        throw new PerformException.Builder()
            .withActionDescription(this.getDescription())
            .withViewDescription(HumanReadables.describe(view))
            .withCause(re)
            .build();
      }

      int duration = ViewConfiguration.getPressedStateDuration();
      // ensures that all work enqueued to process the tap has been run.
      if (duration > 0) {
        uiController.loopMainThreadForAtLeast(duration);
      }
      if (status == Tapper.Status.WARNING) {
        if (rollbackAction.isPresent()) {
          rollbackAction.get().perform(uiController, view);
        } else {
          break;
        }
      }
      loopCount++;
    }
    if (status == Tapper.Status.FAILURE) {
      throw new PerformException.Builder()
        .withActionDescription(this.getDescription())
        .withViewDescription(HumanReadables.describe(view))
        .withCause(new RuntimeException(String.format("Couldn't "
            + "click at: %s,%s precision: %s, %s . Tapper: %s coordinate provider: %s precision " +
            "describer: %s. Tried %s times. With Rollback? %s", coordinates[0], coordinates[1],
            precision[0], precision[1], tapper, coordinatesProvider, precisionDescriber, loopCount,
            rollbackAction.isPresent())))
        .build();
    }

    if (tapper == Tap.SINGLE && view instanceof WebView) {
      // WebViews will not process click events until double tap
      // timeout. Not the best place for this - but good for now.
      uiController.loopMainThreadForAtLeast(ViewConfiguration.getDoubleTapTimeout());
    }
  }

主要的實(shí)現(xiàn)就在status = tapper.sendTap(uiController, coordinates, precision)這句話上,調(diào)用了Tapper的sendTap方法抵知,Tapper實(shí)際就是個點(diǎn)擊器墙基,單擊操作的源碼如下:

  SINGLE {
  @Override
    public Tapper.Status sendTap(UiController uiController, float[] coordinates,
        float[] precision) {
      Tapper.Status stat = sendSingleTap(uiController, coordinates, precision);
      if (Tapper.Status.SUCCESS == stat) {
        // Wait until the touch event was processed by the main thread.
        long singlePressTimeout = (long) (ViewConfiguration.getTapTimeout() * 1.5f);
        uiController.loopMainThreadForAtLeast(singlePressTimeout);
      }
      return stat;
    }
  },

然后是sendSingleTap方法:

  private static Tapper.Status sendSingleTap(UiController uiController,
      float[] coordinates, float[] precision) {
    checkNotNull(uiController);
    checkNotNull(coordinates);
    checkNotNull(precision);
    DownResultHolder res = MotionEvents.sendDown(uiController, coordinates, precision);
    try {
      if (!MotionEvents.sendUp(uiController, res.down)) {
        Log.d(TAG, "Injection of up event as part of the click failed. Send cancel event.");
        MotionEvents.sendCancel(uiController, res.down);
        return Tapper.Status.FAILURE;
      }
    } finally {
      res.down.recycle();
    }
    return res.longPress ? Tapper.Status.WARNING : Tapper.Status.SUCCESS;
  }

可以看到是調(diào)用了MotionEvents的sendDown和sendUp方法模擬了以此點(diǎn)擊操作昔榴,以sendDown為例看看怎么實(shí)現(xiàn)的:

  public static DownResultHolder sendDown(
      UiController uiController, float[] coordinates, float[] precision) {
    checkNotNull(uiController);
    checkNotNull(coordinates);
    checkNotNull(precision);

    for (int retry = 0; retry < MAX_CLICK_ATTEMPTS; retry++) {
      MotionEvent motionEvent = null;
      try {
        // Algorithm of sending click event adopted from android.test.TouchUtils.
        // When the click event was first initiated. Needs to be same for both down and up press
        // events.
        long downTime = SystemClock.uptimeMillis();

        // Down press.
        motionEvent = MotionEvent.obtain(downTime,
            SystemClock.uptimeMillis(),
            MotionEvent.ACTION_DOWN,
            coordinates[0],
            coordinates[1],
            0, // pressure
            1, // size
            0, // metaState
            precision[0], // xPrecision
            precision[1], // yPrecision
            0,  // deviceId
            0); // edgeFlags
        // The down event should be considered a tap if it is long enough to be detected
        // but short enough not to be a long-press. Assume that TapTimeout is set at least
        // twice the detection time for a tap (no need to sleep for the whole TapTimeout since
        // we aren't concerned about scrolling here).
        long isTapAt = downTime + (ViewConfiguration.getTapTimeout() / 2);

        boolean injectEventSucceeded = uiController.injectMotionEvent(motionEvent);

        while (true) {
          long delayToBeTap = isTapAt - SystemClock.uptimeMillis();
          if (delayToBeTap <= 10) {
            break;
          }
          // Sleep only a fraction of the time, since there may be other events in the UI queue
          // that could cause us to start sleeping late, and then oversleep.
          uiController.loopMainThreadForAtLeast(delayToBeTap / 4);
        }

        boolean longPress = false;
        if (SystemClock.uptimeMillis() > (downTime + ViewConfiguration.getLongPressTimeout())) {
          longPress = true;
          Log.e(TAG, "Overslept and turned a tap into a long press");
        }

        if (!injectEventSucceeded) {
          motionEvent.recycle();
          motionEvent = null;
          continue;
        }

        return new DownResultHolder(motionEvent, longPress);
      } catch (InjectEventSecurityException e) {
        throw new PerformException.Builder()
          .withActionDescription("Send down motion event")
          .withViewDescription("unknown") // likely to be replaced by FailureHandler
          .withCause(e)
          .build();
      }
    }
    throw new PerformException.Builder()
      .withActionDescription(String.format("click (after %s attempts)", MAX_CLICK_ATTEMPTS))
      .withViewDescription("unknown") // likely to be replaced by FailureHandler
      .build();
  }

關(guān)鍵語句是boolean injectEventSucceeded = uiController.injectMotionEvent(motionEvent),將MotionEvent通過uiController提交給system service碘橘。我們又回到了UiController的實(shí)現(xiàn)UiControllerImpl,查看其injectMotionEvent方法:

  public boolean injectMotionEvent(final MotionEvent event) throws InjectEventSecurityException {
    checkNotNull(event);
    checkState(Looper.myLooper() == mainLooper, "Expecting to be on main thread!");
    initialize();

    FutureTask<Boolean> injectTask = new SignalingTask<Boolean>(
        new Callable<Boolean>() {
          @Override
          public Boolean call() throws Exception {
            return eventInjector.injectMotionEvent(event);
          }
        },
        IdleCondition.MOTION_INJECTION_HAS_COMPLETED,
        generation);
    keyEventExecutor.submit(injectTask);
    loopUntil(IdleCondition.MOTION_INJECTION_HAS_COMPLETED);
    try {
      checkState(injectTask.isDone(), "Key injection was signaled - but it wasnt done.");
      return injectTask.get();
    } catch (ExecutionException ee) {
      if (ee.getCause() instanceof InjectEventSecurityException) {
        throw (InjectEventSecurityException) ee.getCause();
      } else {
        throw propagate(ee.getCause() != null ? ee.getCause() : ee);
      }
    } catch (InterruptedException neverHappens) {
      // we only call get() after done() is signaled.
      // we should never block.
      throw propagate(neverHappens);
    } finally {
      loopMainThreadUntilIdle();
    }
  }

uiController便通過keyEventExecutor完成了點(diǎn)擊操作的注入互订。

至此我們變完成了Onview方法從前到后,自上而下的分析痘拆,如果讀者足夠細(xì)心會發(fā)現(xiàn)在最為核心的線程池仰禽,執(zhí)行器,事件注入等方面我基本都是一筆帶過的纺蛆,一方面是個人知識儲備有限對這些類的掌握還不算精通吐葵,只能抓住大體思想而無法掌控全局,另一方面是這些底層實(shí)現(xiàn)基本不會涉及到我們對Espresso框架的使用和理解桥氏,有能力的同學(xué)可以去自行研究一下温峭,深刻體會一把Google大神的高端。

通過學(xué)習(xí)Onview方法后的小結(jié)

通過前面幾章的分析和代碼走查字支,我們大概明白了測試方法中onView相關(guān)的語句的具體實(shí)現(xiàn)和邏輯凤藏,讓我們再把這條語句抽出來看看:

onView(withId(R.id.hide_contextual_action_bar)).perform(click());

順手理一遍邏輯:

  • onView方法的參數(shù)是一個Matcher<View>對象,用于作為查找指定控件的匹配器
  • onView方法返回一個ViewInteraction對象堕伪,可針對這個對象做perform操作揖庄,需要傳入ViewAction對象指定操作類型
  • ViewAction對象需要實(shí)現(xiàn)perform方法,調(diào)用UiController對象對之前找到的控件注入指定的操作

從onData方法看相關(guān)實(shí)現(xiàn)

在示例測試方法中我們是從第二句的onView方法看起的欠雌,那么第一句中的onData方法又是怎么回事呢蹄梢?看起來好像結(jié)構(gòu)上和onView方法又有些許相似之處,下面就是來分析這個onData方法的時間了富俄。還是先看下測試方法中的相關(guān)語句:

onData(allOf(instanceOf(Map.class), hasValue(ActionBarTestActivity.class.getSimpleName()))).perform(click());

先看onData方法的實(shí)現(xiàn):

  /**
   * Creates an {@link DataInteraction} for a data object displayed by the application. Use this
   * method to load (into the view hierarchy) items from AdapterView widgets (e.g. ListView).
   *
   * @param dataMatcher a matcher used to find the data object.
   */
  public static DataInteraction onData(Matcher<? extends Object> dataMatcher) {
    return new DataInteraction(dataMatcher);
  }

這次需要的參數(shù)也是一個Matcher禁炒,不過不再限定是基于View的Matcher,返回的是一個DataInteraction對象霍比,從測試方法中看到之后會調(diào)用perform方法幕袱,我們先看這個perform方法:

  public ViewInteraction perform(ViewAction... actions) {
    AdapterDataLoaderAction adapterDataLoaderAction = load();

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

在return語句中我們看到了熟悉的onView方法,那么他的參數(shù)makeTargetMatcher(adapterDataLoaderAction)一定是是一個用于篩選目標(biāo)控件的Matcher<View>對象桂塞,之后的inRoot方法有點(diǎn)陌生凹蜂,我們等下分析以下馍驯,先瞅一眼發(fā)現(xiàn)返回值還是ViewInteraction阁危,那么之后的perform方法就變成了之前熟悉的調(diào)用了,我們重點(diǎn)分析下這兩個沒見過的片段汰瘫。

load方法分析

先看adapterDataLoaderAction參數(shù)狂打,類型為AdapterDataLoaderAction,由load方法生成:

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

繼續(xù)先看adapterDataLoaderAction這個AdapterDataLoaderAction的對象混弥,傳入的3個參數(shù):

  • dataMatcher:即onData方法中傳入的匹配器
  • atPosition:選中匹配的Adapter的第幾項(xiàng)趴乡,默認(rèn)不選擇对省,可以通過atPosition方法設(shè)定
  • adapterViewProtocol:和AdapterView交互的接口,默認(rèn)為AdapterViewProtocols.standardProtocol()晾捏,可以通過usingAdapterViewProtocol方法設(shè)定
    然后看onView的參數(shù)adapterMatcher蒿涎,定義為:
private Matcher<View> adapterMatcher = isAssignableFrom(AdapterView.class);

其實(shí)就是篩選當(dāng)前UI中可以AdapterView的子類,AdapterView是所有內(nèi)容需要使用Adapter來決定的View的基類惦辛,比如ListView劳秋,GridView等有Adapter屬性的視圖都是它的子類,也就是會被篩選器選中胖齐。

找到了篩選條件玻淑,然后接著就是inRoot方法了,我們看看

  public ViewInteraction inRoot(Matcher<Root> rootMatcher) {
    this.rootMatcherRef.set(checkNotNull(rootMatcher));
    return this;
  }

實(shí)際就是給rootMatcher賦值呀伙,rootMatcher的作用我們暫時先不去理會补履,可以看到返回的對象還是原來的ViewInteraction對象,后面調(diào)用perform方法,參數(shù)是我們前面提到過的adapterDataLoaderAction對象剿另,結(jié)合我們分析過的ViewInteraction的流程箫锤,這時會調(diào)用adapterDataLoaderAction的perform方法:

  public void perform(UiController uiController, View view) {
    AdapterView<? extends Adapter> adapterView = (AdapterView<? extends Adapter>) view;
    List<AdapterViewProtocol.AdaptedData> 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++;
    }
  }

代碼略長,大概可以分成這幾個部分雨女,其中3麻汰,4步是同步操作,且僅會操作1次:

  • 對adapterViewProtocol.getDataInAdapterView(adapterView)方法獲得的每一個AdapterViewProtocol.AdaptedData戚篙,使用onData方法中給出的匹配器查找符合要求的項(xiàng)加入到matchedDataItems中
  • 若matchedDataItems為空五鲫,表示沒有匹配項(xiàng),報異常
  • 如果atPosition有設(shè)定值岔擂,data為匹配項(xiàng)的第atPosition個位喂,超出范圍報異常
  • 如果atPosition沒有設(shè)定值,matchedDataItems的數(shù)據(jù)不為1個時報異常乱灵,為1是即為這個data
  • 以!adapterViewProtocol.isDataRenderedWithinAdapterView(adapterView, adaptedData)為條件塑崖,通過adapterViewProtocol.makeDataRenderedWithinAdapterView(adapterView, adaptedData)方法滾動找到data所在的位置

涉及到AdapterViewProtocol的三個方法:getDataInAdapterView,isDataRenderedWithinAdapterView和makeDataRenderedWithinAdapterView痛倚,這里的AdapterViewProtocol默認(rèn)都是StandardAdapterViewProtocol规婆,所以要看下StandardAdapterViewProtocol的這三個方法的具體實(shí)現(xiàn)。

getDataInAdapterView

    public Iterable<AdaptedData> getDataInAdapterView(AdapterView<? extends Adapter> adapterView) {
      List<AdaptedData> datas = Lists.newArrayList();
      for (int i = 0; i < adapterView.getCount(); i++) {
        int position = i;
        Object dataAtPosition = adapterView.getItemAtPosition(position);
        datas.add(
            new AdaptedData.Builder()
              .withDataFunction(new StandardDataFunction(dataAtPosition, position))
              .withOpaqueToken(position)
              .build());
      }
      return datas;
    }

這個很好理解蝉稳,就是通過AdapterView的相關(guān)接口獲取到里面的Adapter數(shù)據(jù)集并用AdaptedData對象封裝了一下

isDataRenderedWithinAdapterView

    public boolean isDataRenderedWithinAdapterView(
        AdapterView<? extends Adapter> adapterView, AdaptedData adaptedData) {
      checkArgument(adaptedData.opaqueToken instanceof Integer, "Not my data: %s", adaptedData);
      int dataPosition = ((Integer) adaptedData.opaqueToken).intValue();
      boolean inView = false;

      if (Range.closed(adapterView.getFirstVisiblePosition(), adapterView.getLastVisiblePosition())
          .contains(dataPosition)) {
        if (adapterView.getFirstVisiblePosition() == adapterView.getLastVisiblePosition()) {
          // thats a huge element.
          inView = true;
        } else {
          inView = isElementFullyRendered(adapterView,
              dataPosition - adapterView.getFirstVisiblePosition());
        }
      }
      if (inView) {
        // stops animations - locks in our x/y location.
        adapterView.setSelection(dataPosition);
      }

      return inView;
    }

這個也簡單抒蚜,根據(jù)篩選到的控件的位置值以及當(dāng)前adapterView顯示的最大最小位置值檢查目標(biāo)控件是否被顯示出來了

makeDataRenderedWithinAdapterView

    public void makeDataRenderedWithinAdapterView(
        AdapterView<? extends Adapter> adapterView, AdaptedData data) {
      checkArgument(data.opaqueToken instanceof Integer, "Not my data: %s", data);
      int position = ((Integer) data.opaqueToken).intValue();

      boolean moved = false;
      // set selection should always work, we can give a little better experience if per subtype
      // though.
      if (Build.VERSION.SDK_INT > 7) {
        if (adapterView instanceof AbsListView) {
          if (Build.VERSION.SDK_INT > 10) {
            ((AbsListView) adapterView).smoothScrollToPositionFromTop(position,
                adapterView.getPaddingTop(), 0);
          } else {
            ((AbsListView) adapterView).smoothScrollToPosition(position);
          }
          moved = true;
        }
        if (Build.VERSION.SDK_INT > 10) {
          if (adapterView instanceof AdapterViewAnimator) {
            if (adapterView instanceof AdapterViewFlipper) {
              ((AdapterViewFlipper) adapterView).stopFlipping();
            }
            ((AdapterViewAnimator) adapterView).setDisplayedChild(position);
            moved = true;
          }
        }
      }
      if (!moved) {
        adapterView.setSelection(position);
      }
    }

根據(jù)SDK不同,直接使用AdapterView的方法滑動到指定的位置上顯示數(shù)據(jù)

以上便是全部的load方法的分析過程耘戚,可以看到load方法實(shí)際作用是定位到滿足給定內(nèi)容的列表項(xiàng)上(通過滾動的方式)嗡髓,這時僅僅是讓我們需要的項(xiàng)出現(xiàn)在屏幕可見范圍,并沒有對其有任何的操作收津。

DataInteraction.perform方法分析

上面我們分析了perform方法中的load方法饿这,下面看它的下半部分:

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

查看makeTargetMatcher的源碼:

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

繼續(xù)看makeTargetMatcher

  private Matcher<View> displayingData(
      final Matcher<View> adapterMatcher,
      final Matcher<? extends Object> dataMatcher,
      final AdapterViewProtocol adapterViewProtocol,
      final AdapterDataLoaderAction adapterDataLoaderAction) {
    checkNotNull(adapterMatcher);
    checkNotNull(dataMatcher);
    checkNotNull(adapterViewProtocol);

    return new TypeSafeMatcher<View>() {
      @Override
      public void describeTo(Description description) {
        description.appendText(" displaying data matching: ");
        dataMatcher.describeTo(description);
        description.appendText(" within adapter view matching: ");
        adapterMatcher.describeTo(description);
      }

      @SuppressWarnings("unchecked")
      @Override
      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<AdaptedData> data = adapterViewProtocol.getDataRenderedByView(
              (AdapterView<? extends Adapter>) parent, view);
          if (data.isPresent()) {
            return adapterDataLoaderAction.getAdaptedData().opaqueToken.equals(
                data.get().opaqueToken);
          }
        }
        return false;
      }
    };
  }

匹配器Matcher的比較是通過matchesSafely方法實(shí)現(xiàn)的浊伙,我們主要看這個方法。它做了這幾個驗(yàn)證:

  • 父View是AdapterView的實(shí)例且不為空
  • onData傳入的匹配器的數(shù)據(jù)能夠和待比較的View的內(nèi)容一致
    實(shí)際就是把之前的條件又做了一次確認(rèn)

后面的childViewMatcher通常是沒有設(shè)定的长捧,相關(guān)邏輯也不會走了嚣鄙,僅在通過DataInteraction.onChildView方法設(shè)定了childViewMatcher后才會做相應(yīng)判斷

所以總結(jié)起來perform方法就是找到AdapterView中的目標(biāo)數(shù)據(jù),并通過滾動操作使其滾動到可見的位置后執(zhí)行給定的ViewAction操作串结,整個onData的流程我們也理清了

從check方法看結(jié)果驗(yàn)證類ViewAssertion的相關(guān)實(shí)現(xiàn)

在測試方法中是有這樣的一句代碼的:

onView(withId(R.id.text_action_bar_result)).check(matches(withText("World")));

在這里面出現(xiàn)了一個新的方法叫check拗慨,其實(shí)無論是DataInteraction還是ViewInteraction都是有check方法的,他們都是用于檢查控件是否滿足測試要求的奉芦,類似與各種單元測試框架中的斷言赵抢,他們的用法是相似的,先看看DataInteraction的check方法的代碼:

  public ViewInteraction check(ViewAssertion assertion) {
     AdapterDataLoaderAction adapterDataLoaderAction = load();

    return onView(makeTargetMatcher(adapterDataLoaderAction))
        .inRoot(rootMatcher)
        .check(assertion);
  }

可以看到DataInteraction的check方法最后也是調(diào)用的ViewInteraction的check方法声功,我么就來分析一下ViewInteraction的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;
  }

前面的內(nèi)容和熟悉烦却,等待主線程Idle,然后執(zhí)行viewAssert.check(targetView, missingViewException)方法先巴,實(shí)際上就是運(yùn)行傳入的ViewAssertion參數(shù)的check方法其爵,比如這里的示例代碼中使用的是matches(withText("World")),matches的源碼如下:

  public static ViewAssertion matches(final Matcher<? super View> viewMatcher) {
    checkNotNull(viewMatcher);
    return new ViewAssertion() {
      @Override
      public void check(View view, NoMatchingViewException noViewException) {
        StringDescription description = new StringDescription();
        description.appendText("'");
        viewMatcher.describeTo(description);
        if (noViewException != null) {
          description.appendText(String.format(
              "' check could not be performed because view '%s' was not found.\n",
              noViewException.getViewMatcherDescription()));
          Log.e(TAG, description.toString());
          throw noViewException;
        } else {
          description.appendText(String.format("' doesn't match the selected view."));
          assertThat(description.toString(), view, viewMatcher);
        }
      }
    };
  }

重點(diǎn)是assertThat(description.toString(), view, viewMatcher)伸蚯,是不是有點(diǎn)斷言的感覺了摩渺?

  public static <T> void assertThat(String message, T actual, Matcher<T> matcher) {
    if (!matcher.matches(actual)) {
      Description description = new StringDescription();
      description.appendText(message)
          .appendText("\nExpected: ")
          .appendDescriptionOf(matcher)
          .appendText("\n     Got: ");
      if (actual instanceof View) {
        description.appendValue(HumanReadables.describe((View) actual));
      } else {
        description.appendValue(actual);
      }
      description.appendText("\n");
      throw new AssertionFailedError(description.toString());
    }
  }

如果控件不能匹配給定的匹配器那么就會報AssertionFailedError錯誤,這里就和斷言一樣了

好像遺漏了匹配器Matcher剂邮?

無論是onView,onData方法還是check方法摇幻,他們的參數(shù)都是Matcher<T>類型的,我把它叫做匹配器挥萌。

Matcher是hamcrest框架的產(chǎn)物绰姻,常用于篩選任務(wù),在一組對象中篩選出滿足指定條件的對象引瀑,通常不推薦直接繼承Matcher實(shí)現(xiàn)而是繼承它的子類BaseMatcher來實(shí)現(xiàn)其功能狂芋。

BaseMatcher共有2個方法需要實(shí)現(xiàn):

  • boolean matches(Object item):Matcher比較的關(guān)鍵方法,返回True表示匹配憨栽,F(xiàn)alse表示不匹配
  • void describeMismatch(Object item, Description mismatchDescription):用于返回該Matcher的描述性信息帜矾,通常用于打印LOG

在android.support.test.espresso.matcher包的ViewMatchers類中Google的大神們幫我們設(shè)定了許多常用的Matcher,例如:isDisplayed屑柔,isCompletelyDisplayed屡萤,hasFocus,withId锯蛀,withText灭衷,withHint次慢,hasLinks等旁涤,大家可以去ViewMatchers類中學(xué)習(xí)翔曲,當(dāng)然你也可以嘗試創(chuàng)建自己的Matcher,用于實(shí)際的測試中

Matcher并不是獨(dú)立使用的劈愚,可以使用anyof()或者allof()來組合若干個Matcher實(shí)現(xiàn)復(fù)雜的匹配器組裝瞳遍,也可以查看hamcrest中的Matchers的源碼查看更多操作Matcher的方法。

Espresso-core庫總結(jié)

經(jīng)過不懈的努力我們終于基本學(xué)會了Espresso-core庫的原理和基本用法菌羽,讓我們再整體來回顧一下:

Espresso框架的應(yīng)用可以分為3個部分掠械,查找控件,執(zhí)行操作注祖,檢查結(jié)果:

  • 查找控件即onView和onData方法猾蒂,用于定位到需要操作的控件,其主要參數(shù)是Matcher匹配器
  • 執(zhí)行操作是針對找到的控件執(zhí)行ViewAction的Perform方法是晨,常用的ViewAction都在android.support.test.espresso.action包下肚菠,也可以自行繼承ViewAction接口實(shí)現(xiàn)想要的操作
  • 檢查結(jié)果使用check方法,其參數(shù)同樣是Matcher匹配器罩缴,同查找控件使用的匹配器一致蚊逢,Google為我們準(zhǔn)備了一些基本的匹配器位于ViewMatchers類中,同時可以使用hamcrest框架中的Matchers類中的方法或者anyof箫章,allof組合方法來實(shí)現(xiàn)更復(fù)雜的匹配器烙荷,或自行創(chuàng)建Matcher對象
  • Espresso框架之所以高效的原因是它并不是運(yùn)行于主線程,并且采取了監(jiān)聽線程檬寂,注入事件的機(jī)制確保第一時間完成指定的操作和檢驗(yàn)且不占用主線程過多的時間终抽,僅在判斷主線程全部資源都是Idle的狀態(tài)下才會執(zhí)行操作
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市桶至,隨后出現(xiàn)的幾起案子拿诸,更是在濱河造成了極大的恐慌,老刑警劉巖塞茅,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件亩码,死亡現(xiàn)場離奇詭異,居然都是意外死亡野瘦,警方通過查閱死者的電腦和手機(jī)描沟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鞭光,“玉大人吏廉,你說我怎么就攤上這事《栊恚” “怎么了席覆?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長汹买。 經(jīng)常有香客問我佩伤,道長聊倔,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任生巡,我火速辦了婚禮耙蔑,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘孤荣。我一直安慰自己甸陌,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布盐股。 她就那樣靜靜地躺著钱豁,像睡著了一般。 火紅的嫁衣襯著肌膚如雪疯汁。 梳的紋絲不亂的頭發(fā)上寥院,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天,我揣著相機(jī)與錄音涛目,去河邊找鬼秸谢。 笑死,一個胖子當(dāng)著我的面吹牛霹肝,可吹牛的內(nèi)容都是我干的估蹄。 我是一名探鬼主播,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼沫换,長吁一口氣:“原來是場噩夢啊……” “哼臭蚁!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起讯赏,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤垮兑,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后漱挎,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體系枪,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年磕谅,在試婚紗的時候發(fā)現(xiàn)自己被綠了私爷。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡膊夹,死狀恐怖衬浑,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情放刨,我是刑警寧澤工秩,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響助币,放射性物質(zhì)發(fā)生泄漏浪听。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一奠支、第九天 我趴在偏房一處隱蔽的房頂上張望馋辈。 院中可真熱鬧抚芦,春花似錦倍谜、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至褥民,卻和暖如春季春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背消返。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工载弄, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人撵颊。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓宇攻,卻偏偏與公主長得像,于是被迫代替她去往敵國和親倡勇。 傳聞我的和親對象是個殘疾皇子逞刷,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內(nèi)容