Rational Functional Tester 測試 Web 應用程式中的常見問題及解決方案
劉 俊濤, 軟體工程師, IBM
Rational® Functional Tester (RFT) 作為自動化測試工具,適用範圍非常廣泛,深入到測試的各個階段:如 UAT(User Acceptance Test,使用者可接受度測試),BVT(Build Verification Test,小版本測試),FVT(Functional Verification Test,功能測試),Migration(遷移測試),Regression(迴歸測試),甚至 GVT(Globalization Verification Test,國際化支援測試)。本文從 Web 應用程式測試的三個常見的問題“多瀏覽器視窗”、“相同的 test object 物件識別”和“Windows 彈出視窗處理”入手,進行具體分析,並給出解決方案。
Rational Functional Tester (RFT) 作為 IBM 自己設計研發的自動化測試工具,適用範圍非常廣泛,僅在 IBM 公司內部,其使用範圍已覆蓋 WPLC 的各大 Web 產品:如 WepSphere Portal,Lotus Quickr,Lotus Connection,Lotus iNotes,Lotus Mashup Center 等,並且深入到測試的各個階段:如 UAT(User Acceptance Test,使用者可接受度測試),BVT(Build Verification Test,小版本測試),FVT(Functional Verification Test,功能測試),Migration(遷移測試),Regression(迴歸測試),甚至 GVT(Globalization Verification Test,國際化支援測試)。測試環境的複雜化、測試用例的多樣化、測試產品的開發特性(如對 Accessibility 的完備支援)等都對自動化程式的開發者帶來困擾和挑戰。本文從 3 個常見的問題入手,進行具體分析,並給出解決方案。
在某些測試用例中,需要同時處理 2 個甚至多個瀏覽器視窗。比如瀏覽器視窗 A 上登入系統的使用者 user1 和瀏覽器視窗 B 上登入系統的使用者 user2 相互聊天,或者 user1 將建立的內容 publish 到另外一個系統上,同時 user1 要在另一個瀏覽器視窗中對釋出到目標系統的內容進行驗證,類似的操作要重複多次。這種情況下,如果指令碼每次只能針對一個瀏覽器進行處理,程式執行的效率將會非常低下。問題的關鍵在於,對於多個瀏覽器視窗,指令碼如何精確區分它們。這裡,以常見的 2 個瀏覽器視窗為例,介紹 3 種區分和處理方法:
ProcessTestObject 是 RFT 中一個比較特殊且有用的類,它是對一個程式物件的封裝。從圖 1 所以的結構層次可以看出,它和 RootTestObject,DomainTestObject 處於同一個層次,直接實現了 TestObject 介面。而一個 ProcessTestObject 物件則是對某個具體程式的抽象,它含有的唯一屬性就是程式 ID,並且只要該程式存在,該物件就有效(該物件的 exists () 和 isActive () 方法指示程式是否結束)。
對於 Browser 來說,每次啟動一個瀏覽器例項,在系統中就產生一個程式,因此可以利用 ProcessTestObject 來區分不同的瀏覽器例項。那麼如何得到每個瀏覽器的 ProcessTestObject 物件呢?注意到 RationalTestObject 有一個靜態方法:startBrowser(String browsername, String url),它返回一個 ProcessTestObject 物件。因此指令碼需要在每次啟動瀏覽器的時候,儲存相應的 ProcessTestObject 物件,指令碼就可以用它來區分和查詢不同瀏覽器例項中的測試物件。示例程式碼如下:
/** * 根據程式物件來區分不同的 browser 例項,進而在該 browser 中查詢特定測試物件 */ public TestObject getTestObject(ProcessTestObject process, String property1, String value1, String property2, String value2){ BrowserTestObject bBrowser=null; TestObject[] browsers=getRootTestObject().find(atProperty(".class","Html.HtmlBrowser")); if(browsers.length==0) return null; for(int i=0; i |
利用 ProcessTestObject 物件來區分瀏覽器視窗僅限於多個獨立 IE 視窗之間,以及多個 IE 視窗和一個 Firefox 視窗的情況。因為:通過 1)宿主 IE 視窗和寄生 IE 視窗(在開啟一個連結的同時,按住 Ctrl 鍵而產生的 IE 視窗)隸屬於同一個程式;2)所有的 Firefox 視窗都隸屬於同一個程式。
目前 RFT 對瀏覽器的支援只包括 IE 和 Firefox,它們同屬於 HTML domain。圖 2 顯示了當前 RFT 所支援的部分 Domain 以及每個 Domain 針對不同應用程式的不同實現。
由圖 2 可知,RFT 對兩種瀏覽器進行了不同的實現。那麼是否存在一種屬性可以在 Domain 實現層次對 IE 和 Firefox 進行區分呢?答案是肯定的。這個屬性就是:Domain 的 ImplementationName。對 IE,該屬性值為 MS Internet Explorer,而對 Firefox,該屬性值為 Firefox。據此,指令碼可以在 Domain 實現層次對 IE 和 Firefox 視窗進行區分。示例程式碼如下:
清單 2. 根據 RFT 對 2 種 browser 在 Domain 層次的不同實現名稱來區分不同的 browser 例項
/** * 根據 RFT 對 2 種 browser 在 Domain 層次的不同實現名稱來區分不同的 browser 例項,進而在該 browser 中查詢特定測試物件。 */ public TestObject getTestObject(boolean bIEOrFF, String property1, String value1, String property2, String value2){ String browserImplementName=bIEOrFF?"MS Internet Explorer":"Firefox"; DomainTestObject[] domains=getDomains(); TestObject root=null; for(int i=0; i |
該方法只適用於區別一個 IE 視窗和一個 Firefox 視窗的情況。對於該種情況,另外一種可行的方法是根據 browser 的名稱來動態查詢。示例程式碼如下:
清單 3. 根據 browser 名稱來區分不同的 browser 例項
/** * 根據 browser 名稱來區分不同的 browser 例項,進而在該 browser 中查詢特定測試物件 */ public TestObject getTestObject(boolean bIEOrFF, String property1, String value1, String property2, String value2){ String browserName=bIEOrFF?"MS Internet Explorer":"Firefox"; TestObject[] browsers=getRootTestObject().find(atDescendant(".class","Html.HtmlBrowser", ".browserName",browserName)); if(browsers.length==0) return null; TestObject[] bjects=browsers[0].find(atDescendant(property1,value1,property2,value2)); if(objects.length>0) return objects[0]; return null; } |
如前 2 中方案所述,可以針對一定情況下的瀏覽器視窗組合進行區別,不能處理所有情況,如 2 個或多個 Firefox 視窗的情況。對於這種情況,可以利用 browser 的一些特殊屬性來進行區分,如瀏覽器視窗的 .window 屬性,任何一個視窗都有唯一的 window ID 來標識它,因此對於 IE 和 Firefox 的任意組合,均可以處理。示例程式碼如下:
清單 4. 根據 browser 視窗的 window ID 來區分不同的 browser 例項
/** * 根據 browser 視窗的 window ID 來區分不同的 browser 例項,進而在該 browser 中查詢特定測試物件 */ public TestObject getBrowser_3(int windowID, String property1, String value1, String property2, String value2 ){ TestObject[] browsers=getRootTestObject().find(atDescendant(".class","Html.HtmlBrowser", ".window",String.valueOf(windowID))); if(browsers.length==0) return null; TestObject[] bjects=browsers[0].find(atDescendant(property1,value1,property2,value2)); if(objects.length>0) return objects[0]; return null; } |
對於臨時出現的 Browser 視窗,還可以利用 BrowserTestObject 的 .documentName,DocumentTestObject 的 .title 和 .url 等來區分:在新視窗出現之前,保留原來視窗的相應屬性值,在新視窗出現之後進行過濾和計算。
針對 Web 應用程式的測試中,經常會遇到在同一個頁面上存在(指顯式存在,即測試人員能肉眼看到。相同屬性的隱藏物件不在此列)“長相“完全一樣的測試物件,如果識別不準確,會導致指令碼誤操作,偏離正常測試邏輯。這種相同物件需要特別留意,通常情況下,指令碼可以首先獲取每個物件的可區分的父物件,進而在該父物件內精確查詢出它。然而還存在父物件也相同的情況,這裡列舉出 3 個例項,並通過對具體例項 HTML 原始碼的分析,解釋不同的方案和特點及其適用範圍。
如果有多個相同的測試物件位於同一個父物件內,且所處的層次 / 深度相同,可利用 index 的計算來區別每一個測試物件。通常 index 標識了該物件在整個頁面結構中的出現次序。如圖 3 示例:
頁面上存在 3 個 CheckBox(圖 3 的原始碼中只列出後 2 個),它們的屬性完全相同,並且他們位於同一個 Table 元素的不同行上,所不同的僅僅是 Table 元素的某一列。因此可以首先根據“Remote portlet”列的值“Remote”計算出該行 CheckBox 的 index 值,也即目標 CheckBox 是當前 parent 物件(Table)中的第幾個 CheckBox,然後利用 find() 方法查詢出 parent 物件中所有的 CheckBox 物件,直接返回第 index 個 Check Box 即可。示例程式碼如下:
/** * 在給定的父物件 parent 中,計算出目標物件的 index,進而通過動態查詢方法直接獲取目標測試物件。 */ public WRadioButton getRadioButton(WTable parent, String column1, String column3){ if(null == parent) return null; int findIndex=-1; for(int i=0; i |
鍵盤操作通常比多次利用 find() 查詢物件要快速有效,但是需要先找到一個合適的錨點。如圖 4 所示,存在兩個屬性一樣的“Portlets”連結,除了通過它們的父物件進行區別,使用鍵盤也是一種選擇:在右上角的翻頁圖示上右擊,然後用“Escape”鍵取消掉彈出的右鍵選單,然後連續 6 次 Tab 鍵就可以將當前焦點移動到右邊的“Portlets”連結上,最後通過Enter鍵“Enter”,即可達到點選“Portlets”連結的目的。
示例程式碼如下:
/** * 通過在參考錨點物件(imageGo)上的鍵盤操作,將輸入焦點移動至目標物件,然後利用Enter鍵達到間接操作目標物件的目的。 */ public void clickPortletByKeyStroke(){ TestObject[] nextImages=getRootTestObject().find(atDescendant(".title", "Go", ".class", "Html.INPUT.image")); if(nextImages.length==0) return; //ESC to disaapear context menu,6 tabs to move focus to "Portlets" link,and Enter to click String keyStroke = "{ESC}{Tab 6}{ENTER}"; GuiTestObject imageGo=(GuiTestObject)nextImages[0]; //right click "Go" image imageGo.click(RationalTestScriptConstants.RIGHT); //type keystroke in current window to click "Portlet" link IWindow topWindow=getScreen().getActiveWindow(); topWindow.activate(); topWindow.inputKeys(keyStroke); } |
針對 Web 控制元件的鍵盤操作依賴於 Web 應用的實現,如果錨點控制元件的右鍵功能被禁止,那麼該方法將無效。同時,還可以考慮左鍵點選,此時要求該 Web 控制元件接受左擊事件時只觸發焦點移動,而沒有其他行為。比如:Login,左擊 Login 文字時,將輸入焦點移動至 Login 所在的 SPAN 元素上,再通過鍵盤操作就可以將輸入焦點切換至目標物件。而針對某些修飾性的文字,如 What’s New, What’s New 只是作為修飾性的文字存在,其根本不能接受任何鍵盤事件和輸入焦點。因此利用鍵盤需要事先明確是否錨點控制元件可利用鍵盤操作來切換焦點。
某些 Html Tag 的特性也可以輔助指令碼來區別和操作相同的測試物件,但這依賴於 Web 應用的開發風格和規範,以及對產品 Accessibility 的支援程度。
對於圖 5 中的 4 個 Radio Button,它們並列位於某一個父物件的同一層次,可以利用 index 計算的方式獲取。同時,注意到每一個 Radio Button 都和一個 Label 元素通過 for 屬性繫結起來 (For 屬性的值對應它所服務元素 -Radio Button 的 id 屬性值 ),因此,直接點選 Label 元素就可以達到選擇相應的 Radio Button 的目的。這可以使指令碼省去複雜的 index 計算。
Label 物件還有一個同樣重要的屬性:ACCESSKEY,它指定了針對服務元素的熱鍵。這同樣可以使指令碼免去動態查詢物件的麻煩,直接使用熱鍵完成對目標控制元件的操作。
示例程式碼如下:
/** * 利用 HTML Label 的 Binding 特性,通過點選 Label 達到操作目標操作物件的目的。 */ public void selectFirstCheckBox(){ TestObject[] labels=getRootTestObject().find(atDescendant(".class","Html.LABEL", ".text","First child")); if(labels.length==0) return; GuiTestObject firstcb=(GuiTestObject)labels[0]; firstcb.click(); } |
Label 的上述特性可以利用至任何擁有 id 屬性的 HTML 標籤。
在自動化指令碼的執行過程中,彈出視窗的出現具有致命威脅,它導致指令碼停滯不前,通常需要人為干預才可以繼續;如果沒有人為干預,指令碼會耗費很長時間,並且以失敗結束。
這裡,彈出視窗可以從兩個方面來進行處理:1)意外視窗或者 sidebar 視窗,這類視窗的出現多可以通過系統配置,尤其是對 browser 的配置,來阻止其彈出,避免其干擾指令碼的正常執行;2)通過指令碼中加入特殊的處理程式碼進行處理,例如,測試用例中要求下載某檔案時,必須對彈出的下載對話方塊進行處理。這類彈出視窗之所以成為問題,是因為 RFT 對某些平臺的控制元件識別能力不足導致的。
- 意外視窗的避免
這裡僅舉 3 個例子,並給出相應的系統配置加以避免:
示例 A.通常在 Firefox 崩潰之後,下次啟動時會出現圖 6 所示對話方塊視窗:
解決方法:在 Firefox 的位址列中,輸入 about:config,開啟配置介面,然後新建 Boolean 型別的如下引數:browser.sessionstore.enabled,並賦值 false;
示例 B.在訪問某些採用了 SSL 協議的 Web 應用程式時,可能會遇到圖 7 所示對話方塊。指令碼要繼續執行,必須選擇”Yes”。:
解決方法:開啟 IE 瀏覽器,進入“Tools”=>“Internet Options”=>“Custom Level…”,使能“Display mixed content”,如下圖 8 所示:
示例 C.如果需要從瀏覽器上下載某些檔案,可能會出現圖 9 所示提示欄,RFT 無法識別它。
解決方法:開啟 IE 瀏覽器,進入“Tools”=>“Internet Options”=>“Custom Level…”,使能“Automatic prompting for file downloads”和“File download”,如圖 10 所示:
這種通過系統配置來限制意外視窗的彈出是自動化指令碼開發人員的首選方案,應該從指令碼執行的標準環境配置的角度來看待和重視它。否則,即使指令碼可以處理,它也需要考慮各種可能出現的視窗,以致於引入不必要的複雜額外邏輯,程式碼臃腫且效率低下。
對於測試用例中必須要處理的系統彈出視窗,處理方式可以有:1)利用 RFT 提供的 IWindow 介面來處理簡單的視窗;2)引入輔助工具來識別複雜視窗控制元件,然後把由輔助工具開發的程式整合進 RFT 來處理彈出視窗。以下分別就圖 11 中下載檔案的例項用 2 種方式進行詳細說明和比較。
IWindow 是 RFT 對 Window 平臺的控制元件的一種抽象描述,所有的控制元件,不論是對話方塊,按鈕,還是輸入框,都是對 IWindow 介面的一種具體實現。這種統一的介面簡化了物件識別,易於操作。
IWindow 利用兩個核心的屬性來表示一個具體的視窗控制元件:Text 屬性和 ClassName 屬性。如圖 12 所示,(&Save, Button) 中,“&Save”是 save 按鈕的 text 屬性,而“Button“表示它是一個按鈕:
圖 12. IWindow 介面對 Windows 對話方塊上的物件識別
在 RFT 中,可以通過 Object Map 這一圖形化工具來直接獲取上圖中所列控制元件的 Text 屬性和 ClassName 屬性。除此之外,還可以通過如下示例程式碼來列印給定視窗中所有 windows 控制元件的識別屬性,進而篩選出指令碼真正需要的。
示例程式碼:printWindowsProperties() 列印視窗 "Save As" 中所有控制元件的屬性:
清單 8. 列印視窗 "Save As" 中所有控制元件的屬性
/** * 該方法列印出”Save As”對話方塊中所有控制元件的 Text 屬性和 ClassName 屬性。 */ public void printWindowsProperties() { // 由於“Save As”視窗唯一,只用 Text 屬性即可找到它,因此設定 //ClassName 屬性為 null; IWindow currentWindow = getWindowByProperties(null, "Save As", null); if (null == currentWindow) return; IWindow[] children = currentWindow.getChildren(); for (int i = 0; i < children.length; i++) { String textProperty = children[i].getText(); String classProperty = children[i].getWindowClassName(); System.out.println("Child[" + i + "]'s properties=(" + textProperty + "," + classProperty + ")"); } } |
上述程式碼中,getWindowByProperties() 獲取給定屬性的 IWindow 控制元件,實現程式碼如下:
/** * 在給定的 parent 視窗中,根據制定的 Text 屬性和 ClassName 屬性查詢控制元件 */ public IWindow getWindowByProperties(IWindow parent, String textProperty, String classNameProperty) { IWindow[] windows = null; if (null == parent) {//parent 為 null,從當前所有視窗中查詢 windows = getTopWindows(); } else {// 從給定的 parent 視窗中查詢給定視窗 windows = parent.getChildren(); } for (int i = 0; i < windows.length; i++) { String _text = windows[i].getText(), _class = windows[i] .getWindowClassName(); boolean find = false; if (null != textProperty) {// 如果 Text 屬性非空 find = _text.matches(textProperty); if (!find)continue; } if (null != classNameProperty) {// 如果 Class Name 屬性非空 find = _class.matches(classNameProperty); } if (find) { return windows[i]; } } return null; } |
因此,針對圖 12 中“Save As”對話方塊,可以用如下程式碼處理:
/** * 該方法首先獲取”Save As”對話方塊,啟用它,然後找到 File name 輸入框,輸入檔案路徑後, * 找到 Save 按鈕,並點選之。等待一段時間後,驗證檔案是否下載成功。 */ public boolean downloadFile(String filepath){ IWindow saveAsWin=getWindowByProperties(null, "Save As", null); if(null == saveAsWin) return false; saveAsWin.activate(); // 通過 ClassName 屬性獲取檔案輸入控制元件物件 IWindow filenameWin=getWindowByProperties(saveAsWin,null, "ComboBoxEx32"); if(null == filenameWin) return false; // 利用鍵盤操作輸入檔案儲存路徑 saveAsWin.inputKeys(filepath); // 利用 Text 和 ClassName 屬性找出 Save 按鈕,並點選 IWindow saveBtn=getWindowByProperties(saveAsWin,"&Save","Button"); if(null == saveBtn) return false; saveBtn.click(); sleep(5); // 檢查檔案是否下載成功 return new File(filepath).exists(); } |
利用 IWindow 介面處理 Windows 視窗,原理簡單,且保證 100% 純 Java 程式碼,同時缺點也是很明顯的:1)需要較多的程式設計;2)IWindow 介面的功能尚有限。對於上述輸入檔案路徑的輸入框,IWindow 沒有提供 setText 方法,只能利用鍵盤操作,並且由於 inputKeys() 方法繼承自 ITopWindow,因此它只對 Top Level 的視窗有效,不能使用 filenameWin inputKeys(filepath) 來輸入檔案路徑。另外,一旦視窗中含有多個輸入框,還需要額外的邏輯先去判斷當前視窗的焦點位置(並且,IWindow 的 hasFocus() 方法並不有效)。
針對 IWindow 處理 Windows 控制元件的缺點,第二種方案是引入一些輔助工具,大部分情況下會起到事半功倍的效果,推薦使用的工具包括:AutoIt(免費使用,但是需要律師的稽核),Rational Robot(IBM 自己的產品 )。這 2 款工具都非常勝任對 Windows 控制元件的識別和支援。
AutoIt 本身就是設計用來在 Windows GUI 中進行自動化操作的工具,對於絕大多數的 Windows 控制元件都可以自動識別,且提供很多的 API 庫供使用者使用,且開發出來的程式可以方便地轉換為 .exe 可執行檔案。本文即用它來演示如何操作圖 12 中的檔案下載對話方塊。
AutoIt 對 Windows 控制元件的識別非常直觀,如圖 13 所示,識別資訊包含“Basic Window Info”(頂層視窗物件資訊)和“Basic Control Info”(頂層視窗內子控制元件物件資訊)兩部分。對於輸入框而言,它的 Class 是 Edit,每個輸入框都有一個唯一的 instance 標識。Class 和 Instance 組成 controlID 就可以在視窗中確定唯一的輸入框。
圖 13. AutoIt 對 Windows 對話方塊的識別
而 AutoIt 提供的豐富的庫函式可以方便的處理 Windows 控制元件。比如:向輸入框中填入資料:ControlSetText ( "title", "text", controlID, "new text"),前 2 個引數唯一標識頂層視窗的標題和包含的文字串,controlID 表示該頂層視窗內的目標輸入控制元件,new text 為輸入內容。因此處理圖 12 中對話方塊的 AutoIt 示例原始碼如下(表示註釋行):
清單 11. 處理圖 12 中對話方塊的 AutoIt 示例原始碼
If $CmdLine[0]<1 Then Exit EndIf handleDownload($CmdLine[1]) ; 方法 handleDownload()接受檔案路徑作為引數,完成下載操作。 Function handleDownload($SaveAsFileName) $title=”Save As” If WinExists($title) Then WinActivate($title) ControlSetText($title,””,”Edit1”, $saveAsFileName) ControlClick($title, “”,”&Save”) Return FileExists($SaveAsFileName) Else Return False EndIf EndFunc |
上述程式碼接受一個輸入引數,由呼叫者指定檔案的存放位置。經過 AutoIt 編譯器編譯之後,可產生可執行檔案 handleDownload.exe。指令碼開發者可以定義一個 java 方法,將之封裝起來,作為公用的方法在以後程式中使用。封裝的示例程式碼如下:
public void handleDownload(String filepath){ String sToolName="x:\\handleDownload.exe"; String sCMD="\""+sToolName+"\""+" "+"\""+filepath+"\""; // 帶輸入引數的命令 try{ java.lang.Process p=Runtime.getRuntime().exe(sCMD); p.waitFor();// 等待 handleDownload.exe 執行結束 }catch(Exception e){ e.printStackTrace(); } } |
Rational Robot 具有類似強大的 windows 控制元件識別和處理功能,且是 ibm 自己開發的產品,非常適合做此類整合,在此不再贅述。
引入輔助工具後,還會涉及到 Java 方法如何獲取 .exe 執行的結果,如何保證 .exe 程式可控等問題。
引入輔助工具後,編碼的效率得到很大提高。表現在:1)輔助工具提供對 Windows 控制元件更強大的支援,能識別和操作絕大多數的 Windows 控制元件;2)程式設計的邏輯清晰簡潔。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/14780914/viewspace-608522/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Android應用安全常見問題及解決方案Android
- Kafka常見的問題及解決方案Kafka
- WordPress:常見問題及解決方案
- 快取常見問題及解決方案快取
- 深度解析移動應用安全的四大常見問題及解決方案
- 【FAQ】推送服務常見問題及解決方案
- 物聯網路卡常見問題及解決方案
- CrashSight 接入上報常見問題及解決方案
- Git常見問題及解決Git
- 【FAQ】整合分析服務的常見問題及解決方案
- 快應用開發常見問題以及解決方案【持續更新】
- 避坑指南:Golang框架自動化測試中的常見問題與解決方案大全Golang框架
- h5移動端常見的問題及解決方案H5
- Nacos 常見問題及解決方法
- UltraEdit常見問題及解決教程
- SAP質量管理模組常見問題及解決方案
- Java™ 教程(常見問題及其解決方案)Java
- 對web應用程式安全的常見誤解Web
- 移動全平臺效能測試工具PerfDog常見問題與解決方案
- 爬蟲常見問題及解決方式爬蟲
- Tomcat常見異常及解決方案程式碼例項Tomcat
- 手機APP測試之ADB常見問題解決方法APP
- 新手linux系統常見問題解決方案Linux
- 幾種常見的身份認證方案和介面測試中的應用
- RecyclerView的使用總結以及常見問題解決方案View
- 使用.Net6中的System.Text.Json遇到幾個常見問題及解決方案JSON
- 測試靈魂三問及解決方案
- As常見問題解決方法
- git常見問題解決Git
- 我們測試了上萬款應用程式,總結了APP測試流程和常見問題APP
- Flutter 疑難雜症系列:鍵盤原理及常見問題解決方案Flutter
- Hadoop常見錯誤及解決方案Hadoop
- 訊息中介軟體應用的常見問題與方案
- Hadoop測試常見問題和測試方法Hadoop
- web開發技巧-網頁排版佈局常見問題及解決辦法Web網頁
- react 記憶體洩露常見問題解決方案React記憶體洩露
- 移動端常見相容性問題解決方案
- SOLIDWORKS常見使用問題解決方案 慧德敏學Solid
- JS中toFixed()方法的問題及解決方案JS