您好,登錄后才能下訂單哦!
從上一章《Robotium源碼分析之Instrumentation進階》中我們了解到了Robotium所基于的Instrumentation的一些進階基礎,比如它注入事件的原理等,但Robotium作為一個測試框架,其功能遠不止于只是方便我們注入事件,其應該還包含其他高級的功能,參照我們前面其他框架如MonkeyRunner,UiAutomator和Appium的源碼分析,我們知道一個移動平臺自動化測試框架的基本功能除了事件注入外起碼還應該有控件獲取的功能。所以,這篇文章我們主要是圍繞Robotium的這幾個功能做闡述。
Robotium作為一個自動化測試框架,做一個自動化測試庫,其所要走的事情就是要封裝好獲取控件和操作控件的各種方法,而因為Robotium編寫的腳本又是和目標測試應用運行在同一進程中的(參考《Robotium源碼分析之Instrumentation進階》第一節),所以就會讓事情更簡單了。比如獲取一個TextView的文本,我們就可以直接在測試腳本這個子線程調用目標控件的個相應方法來獲得文本,如果要設置控件的文本屬性的話,我們也只是需要提供一個runnable讓主線程UiThread去調用控件的設置Text屬性的方法就完成了。當然,如果你是想要模擬用戶鍵盤輸入來設置文本,那么就需要操作事件的另外一個方式,去注入事件發送鍵盤事件來完成了。也就是說,你的腳本既可以運用作為UiThread子線程的優勢來直接操控控件屬性,也可以通過注入事件的方式來模擬用戶層面對控件的操作。
Method | Description | Comment |
Key Events | ||
sendKeySync | 發送一個鍵盤事件,注意同一時間只有一個action,或者是按下,或者是彈起,所有下面其他key相關的事件注入都是以這個方法為基礎的 | |
sendKeyDownUpSync | 基于sendKeySync發送一個按鍵的按下和彈起兩個事件 | |
sendCharacterSync | 發送鍵盤上的一個字符,完整的過程包括一個按下和彈起事件 | |
sendStringSync | 往應用發送一串字符串 | |
Tackball Event | ||
sendTrackballEventSync | 發送軌跡球事件。個人沒有用過,應該是像黑莓的那種軌跡球吧 | |
Pointer Event | ||
sendPointerSync | 發送點擊事件 | |
/* */ public void clickOnText(String text) /* */ { /* 1095 */ this.clicker.clickOnText(text, false, 1, true, 0); /* */ }直接跳轉到Clicker類的clickOnText方法,各個參數的意義也一目了然:
/* */ public void clickOnText(String regex, boolean longClick, int match, boolean scroll, int time) /* */ { /* 427 */ TextView textToClick = this.waiter.waitForText(regex, match, Timeout.getSmallTimeout(), scroll, true, false); /* */ /* 429 */ if (textToClick != null) { /* 430 */ clickOnScreen(textToClick, longClick, time); /* */ /* */ /* */ /* */ } /* 435 */ else if (match > 1) { /* 436 */ Assert.fail(match + " matches of text string: '" + regex + "' are not found!"); /* */ } /* */ else /* */ { /* 440 */ ArrayList<TextView> allTextViews = RobotiumUtils.removeInvisibleViews(this.viewFetcher.getCurrentViews(TextView.class, true)); /* 441 */ allTextViews.addAll(this.webUtils.getTextViewsFromWebView()); /* */ /* 443 */ for (TextView textView : allTextViews) { /* 444 */ Log.d("Robotium", "'" + regex + "' not found. Have found: '" + textView.getText() + "'"); /* */ } /* 446 */ allTextViews = null; /* 447 */ Assert.fail("Text string: '" + regex + "' is not found!"); /* */ } /* */ }
第一步當然是先獲得控件了,具體怎么獲得往后章節會詳細描述。現在重點看430行clickOnScreen方法,注意參數longCilck代表用戶想注入的點擊方法是長按還是短按:
/* */ public void clickOnScreen(View view, boolean longClick, int time) /* */ { /* 182 */ if (view == null) { /* 183 */ Assert.fail("View is null and can therefore not be clicked!"); /* */ } /* 185 */ float[] xyToClick = getClickCoordinates(view); /* 186 */ float x = xyToClick[0]; /* 187 */ float y = xyToClick[1]; /* */ /* 189 */ if ((x == 0.0F) || (y == 0.0F)) { /* 190 */ this.sleeper.sleepMini(); /* */ try { /* 192 */ view = this.viewFetcher.getIdenticalView(view); /* */ } /* */ catch (Exception ignored) {} /* 195 */ if (view != null) { /* 196 */ xyToClick = getClickCoordinates(view); /* 197 */ x = xyToClick[0]; /* 198 */ y = xyToClick[1]; /* */ } /* */ } /* */ /* 202 */ if (longClick) { /* 203 */ clickLongOnScreen(x, y, time, view); /* */ } else { /* 205 */ clickOnScreen(x, y, view); /* */ } /* */ }先根據控件獲得控件點擊坐標:其實就是控件的中心點的絕對坐標值了,該轉換在getClickCoordinates方法進行,沒有什么特別的地方,就不跳進去分析了。
然后根據是否是長按考慮調用clickLongOnScreen或者clickOnScreen方法,我們這里挑clicOnScreen往下展開:
/* */ public void clickOnScreen(float x, float y, View view) /* */ { /* 77 */ boolean successfull = false; /* 78 */ int retry = 0; /* 79 */ SecurityException ex = null; /* */ /* 81 */ while ((!successfull) && (retry < 10)) { /* 82 */ long downTime = SystemClock.uptimeMillis(); /* 83 */ long eventTime = SystemClock.uptimeMillis(); /* 84 */ MotionEvent event = MotionEvent.obtain(downTime, eventTime, 0, x, y, 0); /* */ /* 86 */ MotionEvent event2 = MotionEvent.obtain(downTime, eventTime, 1, x, y, 0); /* */ try /* */ { /* 89 */ this.inst.sendPointerSync(event); /* 90 */ this.inst.sendPointerSync(event2); /* 91 */ successfull = true; /* */ } catch (SecurityException e) { /* 93 */ ex = e; /* 94 */ this.dialogUtils.hideSoftKeyboard(null, false, true); /* 95 */ this.sleeper.sleep(200); /* 96 */ retry++; /* 97 */ View identicalView = this.viewFetcher.getIdenticalView(view); /* 98 */ if (identicalView != null) { /* 99 */ float[] xyToClick = getClickCoordinates(identicalView); /* 100 */ x = xyToClick[0]; /* 101 */ y = xyToClick[1]; /* */ } /* */ } /* */ } /* 105 */ if (!successfull) { /* 106 */ Assert.fail("Click at (" + x + ", " + y + ") can not be completed! (" + (ex != null ? ex.getClass().getName() + ": " + ex.getMessage() : "null") + ")"); /* */ } /* */ }所做的事情就是根據點擊坐標組建兩個分別按下和彈起的事件,然后在89和90行分別調用Instrumentation的sendPointerSync方法觸發InputManager注入這兩個按下和彈起的事件就完了。有了以前文章的基礎,這些代碼分析起來就很流暢了,不然到了這里還要去跟大家解析各種事件注入的情況就會顯得很冗長乏味了。
/* */ public void enterText(EditText editText, String text) /* */ { /* 1748 */ editText = (EditText)this.waiter.waitForView(editText, Timeout.getSmallTimeout()); /* 1749 */ this.textEnterer.setEditText(editText, text); /* */ }首先還是如上獲得控件,然后調用TextEnterer類的setEditText方法:
/* */ public void setEditText(final EditText editText, final String text) /* */ { /* 45 */ if (editText != null) { /* 46 */ final String previousText = editText.getText().toString(); /* */ /* 48 */ this.inst.runOnMainSync(new Runnable() /* */ { /* */ public void run() /* */ { /* 52 */ editText.setInputType(0); /* 53 */ editText.performClick(); /* 54 */ TextEnterer.this.dialogUtils.hideSoftKeyboard(editText, false, false); /* 55 */ if (text.equals("")) { /* 56 */ editText.setText(text); /* */ } else { /* 58 */ editText.setText(previousText + text); /* 59 */ editText.setCursorVisible(false); /* */ } /* */ } /* */ }); /* */ } /* */ }毫無意外的這個方法在48行開始調用的就是runOnMainSync的方法來給主線程執行,所做的事情大致如下:
/* */ public void setProgressBar(ProgressBar progressBar, int progress) /* */ { /* 1691 */ progressBar = (ProgressBar)this.waiter.waitForView(progressBar, Timeout.getSmallTimeout()); /* 1692 */ this.setter.setProgressBar(progressBar, progress); /* */直接跳入到Setter類的setProgressBar方法:
/* */ public void setProgressBar(final ProgressBar progressBar, final int progress) /* */ { /* 101 */ if (progressBar != null) /* */ { /* 103 */ this.activityUtils.getCurrentActivity(false).runOnUiThread(new Runnable() /* */ { /* */ public void run() /* */ { /* */ try { /* 108 */ progressBar.setProgress(progress); /* */ } /* */ catch (Exception ignored) {} /* */ } /* */ }); /* */ } /* */ }103行可以看到是使用了runOnUiThread的方法在主線程直接提交修改控件屬性的消息然后放到UiThread MainLooper來排隊修改進度條屬性的。
本來打算像往常一樣按照自己的邏輯重新分析Robotium獲取控件的原理的,但發現網上已有先驅撰文《Robotium 5.0.1 源碼解析之控件搜索》做了相應的分析了,且近來身體欠佳,牙痛,頭痛,發燒干嘛來襲的,所以就干脆直接引用了,本人覺得寫的還ok,大家閱讀應該不會存在問題的了,這里就先謝過作者了。其實獲取一個控件的方法無非是先獲取得ui界面上得根控件,然后從根控件開始搜索下面指定的控件,在UiAutomator中我們用AccessibilityNodeInfo來封裝一個view,而在Robotium中我們還是使用view和ViewGroup本身而已,注意這里View是描述一個控件的最小單位,而ViewGroup是view的容器,比如最上層的DecorView就是包含了界面所有控件的容器,所以獲得這個容器就能獲得所有的子控件。再次說明,以下分析是摘錄自網上的,本人只做了排版調整。
眾所周知,Robotium是基于Android的單元測試框架Instrumentation,而robotium對于Instrumentation封裝的比較強的地方便是控件搜索,這部分的源碼主要位于ViewFetcher.java中。
要先搜索控件,必須先得到Activity的rootView。在Android中,對于一般的Activity或其對話框,其rootView叫做DecorView,其實就是Activity和Dialog外面的那層框(關于Activity或dialog的層次可以用HierarchyViewer來查看)。
雖然通過Activity類的getWindow().getDecorView可以獲取到Activity自身的DecorView,但是無法獲取到對話框的,因此Robotium中界面控件是從WindowManagerGlobal(或WindowManagerImpl)中的mViews獲取到的。當然mViews中不但包含DecorView,還包含同進程內的所有界面的根節(如懸浮框的根節點)。mView的值的獲取過程主要如下:
1) 確定mViews所在類:android 4.2之前,獲取類為android.view.WindowManagerImpl,4.2及之后,獲取類為WindowManagerGlobal
String windowManagerClassName; if (android.os.Build.VERSION.SDK_INT >= 17) { windowManagerClassName = "android.view.WindowManagerGlobal"; } else { windowManagerClassName = "android.view.WindowManagerImpl"; } windowManager = Class.forName(windowManagerClassName) <span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"> </span>
2). 獲得類的實例:此類是個單例類,有直接的靜態變量可以獲取到其實例, 4.2及之后的版本其變量名為sDefaultWindowManager,3.2至4.1,其變量名為sWindowManager,3.2之前,其變量名為mWindowManager。
/** * Sets the window manager string. */ private void setWindowManagerString(){ if (android.os.Build.VERSION.SDK_INT >= 17) { windowManagerString = "sDefaultWindowManager"; } else if(android.os.Build.VERSION.SDK_INT >= 13) { windowManagerString = "sWindowManager"; } else { windowManagerString = "mWindowManager"; } }
3). 獲取mViews變量的值了,從4.4開始類型變為ArrayList<View>,之前為View[]
viewsField = windowManager.getDeclaredField("mViews"); instanceField = windowManager.getDeclaredField(windowManagerString); viewsField.setAccessible(true); instanceField.setAccessible(true); Object instance = instanceField.get(null); View[] result; if (android.os.Build.VERSION.SDK_INT >= 19) { result = ((ArrayList<View>) viewsField.get(instance)).toArray(new View[0]); } else { result = (View[]) viewsField.get(instance); }
mViews中會包含三種類型的View:
1) 當前顯示的以及沒有顯示的Activity的DecorView
2) 當前對話框的DecorView
3) 懸浮框View等其他不屬于DecorView的獨立View
在搜索控件時,顯然需要在最上層界面中搜索,所以搜索范圍為:
最上層的Activity/Dialog + 懸浮框
對于懸浮框,robotium中的處理是找出mViews中不屬于DecorView類的View,并將其所有子控件引入。
private final View[] getNonDecorViews(View[] views) { View[] decorViews = null; if(views != null) { decorViews = new View[views.length]; int i = 0; View view; for (int j = 0; j < views.length; j++) { view = views[j]; if (view != null && !(view.getClass().getName() .equals("com.android.internal.policy.impl.PhoneWindow$DecorView"))) { decorViews[i] = view; i++; } } } return decorViews; }
對于Activity/Dialog的篩選,Robotium采取對比DrawingTime的方法選出最后繪制的DecorView,其即為最上層Activity/Dialog的DecorView:
/** * Returns the most recent view container * * @param views the views to check * @return the most recent view container */ private final View getRecentContainer(View[] views) { View container = null; long drawingTime = 0; View view; for(int i = 0; i < views.length; i++){ view = views[i]; if (view != null && view.isShown() && view.hasWindowFocus() && view.getDrawingTime() > drawingTime) { container = view; drawingTime = view.getDrawingTime(); } } return container; }
得到懸浮框的根節點和最上層的DecorView后,robotium會將所有View統一添加到一個ArrayList中生成控件列表。添加方法本身很簡單,就是一個簡單的遞歸,但需要注意的是此處有一個onlySufficientlyVisible的判斷。onlySufficientlyVisible是ViewFetcher中最常見的一個變量,其表示是否過濾掉顯示不完全的控件,即onlySufficientlyVisible為true時表示只在顯示完全的控件中搜索目標,為false時表示在所有控件中搜索目標。具體代碼為下面的addChildren函數:
private void addChildren(ArrayList<View> views, ViewGroup viewGroup, boolean onlySufficientlyVisible) { if(viewGroup != null){ for (int i = 0; i < viewGroup.getChildCount(); i++) { final View child = viewGroup.getChildAt(i); if(onlySufficientlyVisible && isViewSufficientlyShown(child)) views.add(child); else if(!onlySufficientlyVisible) views.add(child); if (child instanceof ViewGroup) { addChildren(views, (ViewGroup) child, onlySufficientlyVisible); } } } }
從上面的代碼可以看出,當onlySufficientlyVisible為true時,robotium會對控件的可見不可見進行檢查。不過這里的可見不可見不是指Visible或Invisible(Robotium過濾Invisible控件的方法是RobotiumUtils.removeInvisibleViews,原理是利用view.isShown()方法),而是指由于界面滾動而導致的沒有顯示或顯示不完全。繼續看Robotium對SufficientlyVisible是怎么判斷的:
public final boolean isViewSufficientlyShown(View view){ final int[] xyView = new int[2]; final int[] xyParent = new int[2]; if(view == null) return false; final float viewHeight = view.getHeight(); final View parent = getScrollOrListParent(view); view.getLocationOnScreen(xyView); if(parent == null){ xyParent[1] = 0; } else{ parent.getLocationOnScreen(xyParent); } if(xyView[1] + (viewHeight/2.0f) > getScrollListWindowHeight(view)) return false; else if(xyView[1] + (viewHeight/2.0f) < xyParent[1]) return false; return true; }
xyView[1] + (viewHeight/2.0f) > getScrollListWindowHeight(view)
(xyView[1] + (viewHeight/2.0f) < xyParent[1]
則表示控件有超過一半的面積被隱藏在了父控件的上方,這兩種情況都被Robotium判斷為不滿足SufficientlyVisible的(不過好像沒有判斷橫向的?)。
根據onlySufficientlyVisible過濾掉相應控件后,robotium便完成了控件列表的生成工作,之后的搜索就可直接在列表中進行查找了。
有的時候要搜索指定類型的控件,可以按照類型對控件列表進行再一次的過濾,ViewFetcher中的代碼如下:
public <T extends View> ArrayList<T> getCurrentViews(Class<T> classToFilterBy, View parent) { ArrayList<T> filteredViews = new ArrayList<T>(); List<View> allViews = getViews(parent, true); for(View view : allViews){ if (view != null && classToFilterBy.isAssignableFrom(view.getClass())) { filteredViews.add(classToFilterBy.cast(view)); } } allViews = null; return filteredViews; }
可以看到,robotium直接利用了Class. isAssignableFrom進行類型的匹配。
獲得了控件列表,可以開始搜索指定的目標控件了,先從我們最常用的文本搜索開始,看看robotium的搜索流程。搜索過程的代碼主要位于Searcher.java中,主要功能在兩個searchFor函數中實現,通過嵌套完成目標的搜索。
第一層
<strong> public <T extends TextView> T searchFor(final Class<T> viewClass, final String regex, int expectedMinimumNumberOfMatches, final long timeout, final boolean scroll, final boolean onlyVisible) { //修正非法的expectedMinimumNumberOfMatches if(expectedMinimumNumberOfMatches < 1) { expectedMinimumNumberOfMatches = 1; } //定義一個Callable給下層searchFor使用,可以直接獲取到符合條件的控件列表 final Callable<Collection<T>> viewFetcherCallback = new Callable<Collection<T>>() { @SuppressWarnings("unchecked") public Collection<T> call() throws Exception { sleeper.sleep(); //從當前的Android View中獲取到符合viewClass的控件列表 ArrayList<T> viewsToReturn = viewFetcher.getCurrentViews(viewClass); if(onlyVisible){ //過濾掉Invisible的控件 viewsToReturn = RobotiumUtils.removeInvisibleViews(viewsToReturn); } //robotium支持在webView中查找網頁控件,因此若目標控件是TextView或是TextView的子類, //會把網頁中的文本框也加到控件列表中。 if(viewClass.isAssignableFrom(TextView.class)) { viewsToReturn.addAll((Collection<? extends T>) webUtils.getTextViewsFromWebView()); } return viewsToReturn; } }; try { //調用下層searchFor繼續搜索 return searchFor(viewFetcherCallback, regex, expectedMinimumNumberOfMatches, timeout, scroll); } catch (Exception e) { throw new RuntimeException(e); } } </strong>
這個函數的主要功能有二,一是對非法的expectedMinimumNumberOfMatches進行修正,二是為下一層searchFor提供一個Callable,里面定義好了控件列表的獲取過程。
1) expectedMinimumNumberOfMatches:這個參數表示搜索目標最小發現數目,當一個界面中有多個控件滿足搜索條件,通過此參數可以指定想要獲取的是第幾個。
2) Callable<Collection<T>> viewFetcherCallback:定義了控件列表(即搜索范圍)的獲取過程。首先利用前面提到的viewFetcher.getCurrentViews(viewClass)獲取一個初步的列表;再通過RobotiumUtils.removeInvisibleViews(viewsToReturn)過濾掉不可見控件;最后由于Robotium支持webView內部搜索(Robotium的名字貌似也是來源于Selenium),所以當搜索目標是一個TextView時,Robotium還會調用webUtils.getTextViewsFromWebView()把網頁中的文本框加入到搜索范圍中。
第二層
<strong> public <T extends TextView> T searchFor(Callable<Collection<T>> viewFetcherCallback, String regex, int expectedMinimumNumberOfMatches, long timeout, boolean scroll) throws Exception { final long endTime = SystemClock.uptimeMillis() + timeout; Collection<T> views; while (true) { final boolean timedOut = timeout > 0 && SystemClock.uptimeMillis() > endTime; if(timedOut){ logMatchesFound(regex); return null; } //獲取符合條件的控件列表 views = viewFetcherCallback.call(); for(T view : views){ if (RobotiumUtils.getNumberOfMatches(regex, view, uniqueTextViews) == expectedMinimumNumberOfMatches) { uniqueTextViews.clear(); return view; } } if(scroll && !scroller.scrollDown()){ logMatchesFound(regex); return null; } if(!scroll){ logMatchesFound(regex); return null; } } }</strong>
這一層的主要功能就是循環在控件列表中找到含有指定文本的控件,直至超時或發現了 expectedMinimumNumberOfMatches數目的目標控件,這個過程中需要注意的有四點:
1) uniqueTextViews:為了防止找到的控件存在重復,此處用了一個uniqueTextViews集合來存儲搜索到的結果。
2) 文本的匹配:直接利用了Pattern進行正則匹配,但比對的內容不只包括view.getText(),還包括 view.getError()以及view.getHint()
3) 自動滾動:當開啟了scroll選項,并且在當前的界面沒有找到足夠的目標時,Robotium會自動滾動界面 (不過好像只會向下?):
if(scroll && !scroller.scrollDown()
4) 滾動時robotium只會滾動drawingTime最大的控件(通過ViewFetcher.getFreshestView()),所以一個界面中有兩個可滾動控件時,robotium只會滾動其中一個。
作者 | 自主博客 | 微信 | CSDN |
天地會珠海分舵 | http://techgogogo.com | 服務號:TechGoGoGo 掃描碼:
| 向AI問一下細節 推薦閱讀:
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。 猜你喜歡最新資訊相關推薦相關標簽AI
助 手
夏津县|
辉县市|
科尔|
清镇市|
南皮县|
始兴县|
镇赉县|
巴彦县|
余姚市|
太湖县|
措美县|
焦作市|
正蓝旗|
沿河|
安多县|
嘉义县|
天门市|
察雅县|
庄河市|
车致|
星子县|
卢龙县|
林口县|
巩义市|
揭东县|
财经|
霍林郭勒市|
西宁市|
鄂温|
固安县|
综艺|
广平县|
竹北市|
商都县|
鹿泉市|
清河县|
寿阳县|
镇原县|
普宁市|
舒城县|
巍山|
|