腾讯Android自动化测试实战3.1.4 Robotium的控件获取、操作及断言
3.1.4 Robotium的控件獲取、操作及斷言
Robotium是一款在Android客戶端中的自動化測試框架,它需要模擬用戶操作手機屏幕。要完成對手機的模擬操作,應該包含以下幾個基本操作:
(1)需要知道所要操作控件的坐標。
(2)對要操作的控件進行模擬操作。
(3)判斷操作完成后的結果是否符合預期。
因此,本節將從控件獲取、控件操作及操作后斷言來介紹Robotium,此外,由于WebView在控件獲取和控件操作上都與Native完全不同,將對其做單獨介紹。
1. Native控件獲取
從Robotium中獲取Native控件主要有兩大方式:一個是根據被測應用的控件ID來獲取;另一個是先獲取當前界面所有的控件,對這些控件進行過濾封裝后再提供相應的獲取控件的API。
1)根據被測應用的控件ID來獲取
根據控件ID獲取見表3-1。
根據String型ID獲取控件:
ImageView mIcon = (ImageView) solo.getView("mypic");
在Android中,所有的控件都繼承自View,因此,如果被測應用中的控件有唯一ID的話,就可以使用這種通過ID形式唯一獲取所要操作的控件。
例如獲取RelativeLayout或LinearLayout:
RelativeLayout rel = (RelativeLayout) solo.getView("example1");
LinearLayout lin = (LinearLayout) solo.getView("example2");
由于Android中所有的控件都繼承自View類,而對于開發人員的自定義控件,這些自定義控件也基本是繼承自Android的基礎控件擴展而來的,因此通過這種方式幾乎可以獲得所有類型的控件,獲取相應類型的控件時只要進行轉義即可,因此當控件擁有唯一ID時,推薦使用該方式。
控件ID可以通過Android SDK中提供的工具來查看,例如%ANDROID_HOME%\tools\uiautomatorviewer.bat工具,在Android 4.3及以上系統版本的手機上,可直接查看到UI界面的ID。
2)根據控件類型的索引、文本來獲取
根據文本獲取見表3-2。
此方式是Robotium先將當前界面中的所有控件全部獲取,然后按控件類型、索引進行過濾后再獲取指定的控件View。
根據index索引獲取控件:
//返回界面中第一個類型為Button的控件
Button loginBtn = (Button) solo.getButton(0);
其他的如getEditText(int index)、getText(int index)均同理。
根據文本text獲取控件:
//返回界面中文本為‘登錄’類型為Button的控件
Button loginBtn = (Button) solo.getButton("登錄");
其他的如getText(String text)、getEditText(String text)均同理。
在Robotium中查找控件時,如果找不到相應ID或文本的控件,測試框架會throw出“View with id ××× is no found”或者“with text ××× no found”等Throwable異常,若我們并不希望因此而報錯,則可以使用try catch Throwable來捕獲。
3)根據控件類型進行過濾
根據類型過濾見表3-3。
表3-3 根據類型過濾
返回值? 方法及說明
ArrayList<View>????? getCurrentViews()
獲取當前界面或彈框中所有的控件
ArrayList<T>???? getCurrentViews(Class<T> classToFilterBy)
獲取當前界面或彈框中所有控件類型為classToFilterBy的控件
ArrayList<T>???? getCurrentViews(Class<T> classToFilterBy, View parent)
獲取父控件parent下所有控件類型為classToFilterBy的控件
?
獲取當前界面或彈框中所有控件類型為TextView的控件:
ArrayList<TextView> allTextViews = solo
????????????? .getCurrentViews(TextView.class);
獲取指定父控件下所有控件類型為TextView的控件:
RelativeLayout rel = (RelativeLayout) solo.getView("example1");
ArrayList<TextView> allTextViews = solo
????????????? .getCurrentViews(TextView.class, rel);
同樣是過濾出指定的控件類型,不過該方法是從父視圖parent中開始過濾,當不指定parent,即solo.getCurrentViews(TextView.class,null)時,則和solo.getCurrentViews(TextView.class)一樣,返回的是當前界面中所有的。
移動App一般節奏很快,UI布局結構也經常隨著功能的變更而變動,例如“登錄”按鈕從最上面變到了最下面,因此通過索引或文本來獲取控件是有很大隱患的。很多時候,通過巧妙地控件過濾可以更準確地找到相應的控件。
2. Native控件操作
對于Android端的自動化測試而言,當我們獲取到期望的控件后,接下來就是對該控件進行點擊、長按、文本輸入、拖動等模擬操作。除此之外,UI自動化測試為了貼近用戶的真實使用及自身健壯性,還需要時延等待、頁面加載等待;為了判斷界面是否符合預期,則還需要控件搜索、界面截圖等操作。
1)點擊、長按操作
點擊長按見表3-4。
表3-4 點擊長按
返回值? 方法及說明
void????? clickOnView(View view)/clickLongOnView(View view)
點擊指定的View控件/長按指定的View控件
void????? clickOnScreen(float x, float y)/clickLongOnScreen(float x, float y)
根據坐標x, y點擊屏幕/根據坐標x, y長按屏幕
?
Robotium是基于控件的自動化測試框架,當獲取到要操作的控件后,直接對控件進行點擊、長按或文本輸入等操作即可。
點擊指定的View控件:
Button loginBtn = (Button) solo.getView("loginBtn");
solo.clickOnView(loginBtn)
Robotium還提供了點擊文本、點擊圖片的API,例如clickOnText(String text)、click-OnButton(String text)等,這類API類似于前文所介紹的,先根據文本獲取控件,再發送點擊事件:
Button loginBtn = (Button) solo.getButton("登錄");
solo.clickOnView(loginBtn)
類似于點擊、長按指定的View控件:
Button loginBtn = (Button) solo.getButton("登錄");
solo.clickLongOnView(loginBtn)
需要注意的是,Robotium的點擊事件是通過Instrumentation發送的,因此該類點擊方法不能點擊非被測應用的區域,例如不能點擊至通知欄所在的區域,否則會出現類似如下的異常:
java.lang.SecurityException: "Injecting to another application requires INJECT_EVENTS permission"
因此在使用Robotium編寫測試用例時,需要注意其無法跨應用的缺點,從而盡量避免出現此場景,有些場景偶然性地無法規避,可以采用try catch Throwable的形式捕獲異常,而對于需要跨應用的場景,則可以使用9.4.2節介紹的UI Automator結合Instrumentation模式進行處理。
try {
??? } catch (Throwable e) {
}
在手機設置–開發者選項中,可以開啟“指針位置”,開啟“指針位置”后,再觸摸屏幕時,可實時顯示屏幕坐標。調試時為了更準確地知道對屏幕的什么地方進行了操作,也常常同時開啟“顯示觸摸操作”開關。
2)操作輸入框
操作輸入框見表3-5。
表3-5 操作輸入框
返回值? 方法及說明
void????? enterText(EditText editText, String text)
在指定的EditText中輸入文本text
void????? typeText(EditText editText, String text)
在指定的EditText中鍵入文本text
void????? clearEditText(EditText editText)
清空指定的輸入框
?
在自動化測試過程中,當我們可以準確獲取控件,并能模擬點擊、長按等基本操作后,就可以在被測應用中進行自由跳轉,然后可能就需要進行一些輸入操作。測試框架中主要提供了enterText(EditText editText, String text)和typeText(EditText editText, String text)兩種方法,前者直接對EditText文本框進行賦值,不會有文本輸入的展示過程,而后者則會一個文本一個文本地輸入,更貼近真實用戶的操作。
EditText userET = (EditText) solo.getView("example_et_id");
solo.enterText(userET, "my_user_name")????? //直接對文本框賦值
solo.typeText(userET, "my_user_name")?????? //會展示輸入的過程
3)滑動、滾動
滑動、滾動見表3-6。
表3-6 滑動、滾動
返回值? 方法及說明
void????? drag(float fromX, float toX, float fromY, float toY, int stepCount)
從起始x,y坐標滑至終點x,y坐標;通過stepCount參數指定滑動時的步長
void????? scrollToTop() / scrollToBottom()
滾動至頂部 / 滾動至底部
void????? scrollUp() / scrollDown()
向上滾動屏幕 / 向下滾動屏幕
void????? scrollListToLine(AbsListView absListView, int line)
滾動列表至第line行
?
在Android中,常用的操作還有各種滑動手勢,如上拉、下拉、左滑、右滑等。在滑動方面,測試框架主要提供了兩類支持,一類是根據坐標進行滑動從而可以模擬各類手勢操作,另一類則是根據控件來直接進行滾動操作。
根據坐標進行滑動的主要是drag(float fromX, float toX, float fromY, float toY, int step Count),這里的參數包括起始位置的x與y坐標、終點位置的x與y坐標,還有步長stepCount。其中步長stepCount的意思是,假如要從A點滑到B點,如果步長為1,那么將直接產生從A點到B點的手勢操作,滑動速度很快;如果步長為100,則將從A到B分成100等份,例如A、A1、A2…B,然后依次從A滑到A1,再從A1滑到A2、A2滑到A3……這樣滑動更慢但結果也更精確,例如當我們在手機上快速從下往上滑動時,列表滑動是有慣性的,會快速滾動,而這常常不是我們所需要的。
根據控件進行滾動主要有滾動至頂部、底部等方法。scrollToTop()方法可以將當前屏幕滑至頂部,如果當前是ListView則滑至列表的頂部,如果是WebView則滑至頁面的頂部。同樣地,scrollToBottom()可將界面滑至底部。類似的還有向下滑一屏的scrollDown()方法和向上滑一屏的scrollUp()方法。與前文介紹的drag方法不同的是,這類滾動調用的是相應控件自身的API,例如WebView的滾動調用的是控件自身的pageUp(boolean top)或pageDown(boolean bottom)方法。因此,這種方式與drag方式最大的區別在于,drag是實際地模擬手勢操作,當上拉時,如果ListView有監聽上拉加載更多,那么使用drag是可以觸發上拉加載更多的,而scrollUp()則不能。
4)搜索與等待
搜索與等待見表3-7。
表3-7 搜索與等待
返回值? 方法及說明
void????? sleep(int time)
休眠指定的時間,單位毫秒
boolean searchText(String text)
從當前界面搜索指定文本
boolean waitForView(int id) / waitForText(String text)
等待指定控件出現 / 等待指定文本出現
boolean waitForActivity(String name)
等待指定的Activity出現
boolean waitForLogMessage(String logMessage)
等待指定的日志信息出現
boolean waitForDialogToOpen() / waitForDialogToClose()
等待彈框打開 / 等待彈框關閉
?
UI自動化測試常常被詬病運行不穩定,除了項目快速迭代導致界面經常變更這一不可控因素外,腳本常常運行出錯就是由于沒有合適的等待機制導致控件未找到、點擊異常等問題,要想測試用例能夠快速且穩定地運行,合理使用等待是關鍵要素之一。
Robotium中提供了諸多與等待相關的API,但是實際情況中需要等待的操作往往要復雜得多,因此測試框架中也提供了Condition模式,即waitForCondition(Condition condition, int timeout)方法,使用該方法時,實現Condition接口并重寫isSatisfied()方法,isSatisfied()為true時將跳出等待。通過這種模式我們可以自定義實現更多類型的等待操作,如代碼清單3-1所示。
代碼清單3-1 使用waitForCondition模式實現等待
public void waitForAppInstalled(final String appName, int timeout) {
??? waitForCondition(new Condition() {
??????? @Override
??????? public boolean isSatisfied() {
??????????? sleeper.sleepMini();
??????????? return checker.isAppInstalled(appName);
??????? }
??? }, timeout);
}
當然了,我們也可以使用超時機制來實現,如代碼清單3-2所示。
代碼清單3-2 使用TimeOut模式實現等待
public void waitForAppInstalled(final String appName, int timeout) {
??? long endTime = SystemClock.uptimeMillis() + timeout;
??? while (SystemClock.uptimeMillis() < endTime) {
??????? if (checker.isAppInstalled(appName)) {
??????????? break;
??????? }
??????? sleeper.sleep();
??? }
}
需要注意的是,Robotium中查找控件、點擊控件等API都默認使用了搜索與等待機制,當我們使用上文提到的獲取控件、點擊控件相關操作時,測試框架已經做好了等待操作,因此非特殊情況是不需要額外增加等待操作的步驟的。太多的等待將使用例執行變得緩慢低效,因此在用例編寫調試過程中應該做好平衡。
5)截圖及其他
截圖及其他見表3-8所示。
表3-8 截圖及其他
返回值? 方法及說明
void????? takeScreenshot(String name)
截圖,圖片名稱為指定的name參數,圖片默認路徑為/sdcard/Robotium-Screenshots/
void????? finishOpenedActivities()
關閉當前已打開的所有Activity
void????? goBack() / goBackToActivity(String name)
點擊返回鍵 / 不斷地點擊返回鍵直至返回到指定的Activity
void????? hideSoftKeyboard()
收起鍵盤
void????? setActivityOrientation(int orientation)
等待設置Activity轉屏方向
?
自動化測試過程中,因為都是自動化執行的,當用例執行失敗時,除了日志外,最方便解決定位問題的就是運行時的截圖,有了截圖定位問題往往事半功倍,Robotium中提供了單次截圖及截取一系列圖片的功能。takeScreenshot()方法可以直接截取當前屏幕,并將其默認地保存在/sdcard/Robotium-Screenshots/目錄下,要更改圖片名稱則使用takeScreenshot(String name),要截取某時間段內一個序列的話則可以使用startScreenshotSequence(String name)。那么如何更好地在自動化中使用截圖功能呢?一般情況下我們更希望的是在用例執行失敗時進行截圖,詳情請見本書9.3.2節中介紹的結合Spoon出錯重試與截圖。
除了常規的操作外,Robotium測試框架還提供了發送模擬按鍵sendKey(int key)、設置屏幕是橫屏還是豎屏setActivityOrientation(int orientation)、模擬點擊返回鍵goBack()、跳轉至指定Activity的方法goBackToActivity(String name)、收起輸入法hideSoftKeyboard()、關閉所有已打開的Activity 的方法finishOpenedActivities()等。通過組合利用這些常用操作,基本就可以完成在Android端的UI自動化操作了。
3. WebView支持
在Android App中由于HTML可以更快地響應變化,而不像Native那樣需要發布版本才能讓用戶使用上新特性,因此大多數App都是既有Native部分,也有HTML部分,也即俗稱的Hybrid App。而Robotium在Robotium4.0版本中就開始全面支持WebView的自動化了。要了解如何使用Robotium測試框架來對App中的WebView部分進行自動化測試,首先需要了解HTML基礎,然后了解Robotium是如何獲取頁面元素并進行操作的。
1)HTML基礎
Robotium支持通過ID、className等方式來獲取WebElement元素,因此,首先了解ID、className等的概念,模擬打開GitHub首頁并查看網頁源碼如圖3-12所示。
HTML元素:指的是從開始標簽到結束標簽的所有代碼。如圖3-12所示,Sign in按鈕在開始標簽<a href="/login" class="btn btn-block primary">與結束標簽</a>內,因此整體屬于一個HTML元素。
HTML屬性:屬性總是以名稱/值對的形式出現的,比如:name="value"。屬性總是在HTML元素的開始標簽中規定的。核心屬性有class(規定元素的類名)、ID(規定元素的唯一ID)。Sign in按鈕中就有class屬性,class="btn btn-block primary"。
?
?
圖3-12 GitHub首頁的HTML結構
2)WebElement相關API及操作
WebElement相關API見表3-9。
表3-9 WebElement相關API
返回值? 方法及說明
ArrayList<WebElement> getCurrentWebElements()
獲取當前WebView的所有WebElement元素
ArrayList<WebElement> getCurrentWebElements(By by)
通過By根據指定的元素屬性獲取當前WebView的所有WebElement元素
void????? clickOnWebElement(By by)
通過By根據指定的元素屬性點擊WebElement
void????? clickOnWebElement(WebElement webElement)
點擊指定的WebElement
void????? enterTextInWebElement(By by, String text)
根據by找到WebElement,并輸入指定的文本text
boolean waitForWebElement(By by)
等待根據by獲得的WebElement出現
?
在Robotium中對WebElement進行操作有兩種方式,一種是先獲取相應的WebElement,然后發送點擊事件,另一種則是直接調用clickOnWebElement(By by)進行點擊。
在獲取WebElement元素前我們首先需要知道這個頁面的HTML結構,需要知道URL鏈接才能方便地查看HTML元素、屬性等。
獲取WebView中的頁面信息可以參考本書6.3.3節Appium 腳本常見問題及處理方法中如何獲取WebView中的頁面信息這一部分內容,通過Chrome瀏覽器中的DevTools工具可以快速方便地查看WebView中的信息。
我們也可以使用原始的如代碼清單3-3所示的方式打印出所有的元素信息。
代碼清單3-3 使用日志打印方式獲取元素信息
ArrayList<WebElement> webElements = solo.getCurrentWebElements();
WebElement webElement = null;
for(int i=0;i< webElements.size();i++){
??? webElement = webElements.get(i);
??? Log.i("WebElement", "getId:" + webElement.getId());
??? Log.i("WebElement","getClassName:"+webElement.getClassName());
??? Log.i ("WebElement", "getText:" + webElement.getText());
}
當我們知道了相應頁面的元素、屬性后,就可以通過元素或屬性等信息來獲取指定的WebElement。
1)獲取當前WebView所有WebElement
ArrayList<WebElement> webElements = solo.getCurrentWebElements();
2)通過className獲取
ArrayList<WebElement> signIns = solo.getCurrentWebElements(By
???????????? .className("btn btn-block primary"));
3)通過ID獲取
ArrayList<WebElement> signIns = solo.getCurrentWebElements(By
???????????? .id("example_id"));
4)通過textContent獲取
ArrayList<WebElement> signIns = solo.getCurrentWebElements(By
???????????? .textContent("Sign in"));
類似的還有通過cssSelector、name、tagName、xpath等方式獲取。
5)通過WebElement點擊
拿到WebElement后,如果在頁面中該標識是唯一的,那么數組長度為1,可以通過clickOnWebElement(WebElement webElement)方法比較精確地點擊。
solo.clickOnWebElement(signIns.get(0));
以上獲取WebElement并點擊也可以直接使用clickOnWebElement(By by)方法完成。
solo.clickOnWebElement(By.className("btn btn-block primary"));
6)WebElement輸入
solo.enterTextInWebElement(By.name("userId"), "your username");?
solo.enterTextInWebElement(By.name("passwd"), "your passwd");?
同樣地,WebElement也支持等待操作,可以通過waitForWebElement(By by)等待相應的元素出現,然后查找,這樣可以使腳本更健壯。不過Robotium中的clickOnTx-WebElement(By by)也均默認已經使用了等待機制,因此非特殊情況,腳本中不需要額外增加等待操作。
Robotium中對WebView的支持由于是使用對系統WebView執行JS從而封裝獲取頁面元素的方式,因此該測試框架只支持App中使用系統WebView的情況,如果App或瀏覽器使用的是非系統內核的WebView,例如騰訊手機QQ瀏覽器的X5內核,則無法使用,需要引用X5的SDK并對Robotium進行改造才可支持。
4. 斷言
自動化測試中,我們獲取控件、執行操作后,接下來就是要對操作后的場景進行斷言了。Robotium是基于Instrumentation的測試框架,其測試用例編寫的框架是基于Junit的,因此,本小節將先介紹Junit中的斷言,然后介紹Robotium中適用于Android端自動化的斷言。
1)Junit中的斷言
Junit中的斷言相關API見表3-10。
表3-10 Junit中的斷言相關API
返回值? 方法及說明
void????? assertTrue(String message, boolean condition)
斷言傳入的condition參數應該為True,否則將拋出一個帶有message提示的Throwable異常
void????? assertFalse(String message, boolean condition)
斷言傳入的condition參數應該為False,否則將拋出一個帶有message提示的Throwable異常
void????? fail(String message)
直接使用例失敗,并拋出一個帶有message提示的Throawable異常
?
Junit中的斷言可以查看Android SDK中junit.framework.Assert包下的Assert類,常用的有assertTrue(String message, boolean condition)方法,即斷言方法中第二個參數condition的結果是否為True,如果為True則該語句執行通過,否則該語句將拋出Throwable的異常,而異常中的提示語將為第一個參數message。因此,使用斷言時,應該準確明了地說明message參數,以便斷言不符合預期時可以快速判斷是什么原因導致的。例如斷言某個控件應該要顯示在界面中,代碼如下:
Button loginBtn = (Button) solo.getView("loginBtn");
assertTrue("‘登錄’按鈕應該要顯示在界面", loginBtn.isShown());
同樣地,還有assertFalse(String message, boolean condition)方法,用于斷言第二個條件中的結果應該為False。通過這兩個方法,只要測試過程中的預期結果能轉換成True或False的都可以進行判斷,例如判斷界面元素是否顯示、數值大小比較、文本對比等。
在測試工程中,當出現某種場景時,有時我們希望直接使用例失敗而不再往下執行,此時可以使用Assert類中的fail(String message)方法,例如:
if(isBadHappened()){
???? fail("this should no happened");
}
而如果出現某種場景,我們希望直接使用例通過而不再執行,則此時在用例腳本中直接使用return即可。
2)Robotium中的斷言
Robotium中的斷言相關API見表3-11。
表3-11 Robotium中的斷言相關API
返回值? 方法及說明
void????? assertCurrentActivity(String message, String name)
斷言當前界面是否為name參數指定的Activity,若不是則拋出一個帶有message提示的Throwable異常
void????? assertMemoryNotLow()
斷言當前是否處于低內存狀態
?
Robotium基于Junit中的斷言判斷,也封裝了幾個方便在Android端自動化時使用的斷言方法。例如assertCurrentActivity(String message, String name)方法可以判斷當前界面是否是預期的Activity,我們知道Android中許多頁面都對應于一個Activity,當App跳轉到一個界面時,就可以使用該方法來判斷是否已跳轉到相應Activity了。
//獲取當前的Activity名
String currentActivity =
?????????? ???solo.getCurrentActivity().getClass().getSimpleName();
// expectedActivity為期望跳轉的Activity
solo.assertCurrentActivity("expected xxxActivity" + " but was " + currentActivity, expectedActivity);
另外,測試框架中的assertMemoryNotLow()方法可以用來判斷當前是否處于內存吃緊的情況。在Robotium封裝的斷言API并不多,因為如前文所說,大多數場景都可以使用True或False來進行判斷。
3)Android中的斷言
Android中的斷言相關API見表3-12。
表3-12 Android中的斷言相關API
返回值? 方法及說明
void????? assertOnScreen(View origin, View view)
斷言view是否在屏幕中
void????? assertBottomAligned(View first, View second)
斷言兩個view是否底端對齊,即它們的底端y坐標相等
?
在Android SDK中,android.test.ViewAsserts包下有個ViewAsserts類可以方便地進行與控件相關的斷言。例如斷言控件是否在窗口中assertOnScreen(View origin, View view),斷言兩個控件是否底部對齊assertBottomAligned(View first, View second),是否右對齊assertRightAligned(View first, View second),等等。而之所以能實現這些斷言在于View控件本身就具有非常多的可以用于判斷自身狀態的屬性,例如View可以判斷自身是否顯示isShown(),判斷是否被選中isSelected(),還可以獲取自身所在的坐標位置getLocationOnScreen(int[] location)和寬高getWidth()、getHeight(),等等。由于基于Robotium編寫的測試用例是以App形式安裝進手機的,且運行時是運行在被測應用所在的進程,因此我們使用斷言時,可以借助Android SDK中豐富的類庫來進行各種判斷,例如判斷當前網絡狀態、應用安裝情況、當前應用是否處于前臺等,可以很方便地對測試的預期結果進行判斷。如代碼清單3-4所示,調用Android中的API根據包名判斷是否是系統應用。
代碼清單3-4 根據包名判斷是否是系統應用
/**
?* 根據packageName判斷該應用是否是系統應用
?* @param packageName? 應用的包名
?* @return? true,系統應用;false,非系統應用
?*/
public boolean isSystemApp(String packageName){????
??? PackageManager pm = getInstrumentation().getTargetContext().getApplicationContext().getPackageManager();
?
??? ApplicationInfo applicationInfo = null;
?
??? try {
??????? applicationInfo = pm.getApplicationInfo(packageName, PackageManager.GET_UNINSTALLED_PACKAGES);
??????? if(applicationInfo !=null &&? (applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) ==1){
??????? LogUtils.logD(TAG, "applicationInfo flag:" + (applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM));
??????????? return true;
??????? }
?
??? } catch (NameNotFoundException e) {
??? }??????
???
??? return false;
}
總結
以上是生活随笔為你收集整理的腾讯Android自动化测试实战3.1.4 Robotium的控件获取、操作及断言的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 《C语言点滴》一1.5 内功修炼
- 下一篇: 《设计团队协作权威指南》—第1章1.3节