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í)行操作