巧用 Rational Functional Tester 的 IWindow 介面

myattitude發表於2009-04-14

Rational Functional Tester (RFT) 是跨平臺的 GUI 自動測試工具,可以自動測試基於 Java, HTML 和 .Net 的應用程式。RFT 的大部分功能是通過物件對映或者動態查詢得到的 TestObject 實現。同時,RFT 還提供了一個基於 Win32 API 和 Linux Xlib 的 IWindow 介面,可以用來處理原生控制元件(指使用系統 API 直接生成的控制元件)和窗體。當使用 RFT 來測試 Java 或者 Web 應用程式時,可能也需要處理到系統原生的對話方塊,比如檔案對話方塊或訊息對話方塊。Windows 平臺上的原生對話方塊和其中的控制元件可以被對映成 Win domain 的 TestObject (*1),但是 Win domain 並不支援 Linux 平臺。在需要跨平臺測試的情況下,需要使用 IWindow 來處理原生控制元件。

IWindow 的功能

雖然 IWindow 類只能對原生控制元件做很基礎的操作,但是可以被用來做很多事情。比如,可以對控制元件點選,輸入文字,檢查是否顯示和是否有焦點。此外,配合 JNI 和 IPC 的 C 語言程式碼,IWindow 可以實現測試中的所有需求。如何使用 IPC 和 JNI 不在本文的討論範圍中,只會在總結的部分簡要提到。

一般來說,我們需要處理 6 種控制元件: Buttons、Labels、Check Boxes、Radio Buttons、Text Fields、和 Combo Boxes 。下面對每種控制元件分別討論:

Button

對於 Button 控制元件,在 GUI 測試中關心的是上面顯示的文字和如何對它進行點選。它們都可以直接通過 IWindow 的方法實現。明確地說,就是分別使用 getText 和 click 方法。

Label

只需要知道上面顯示的文字,可以使用 getText 方法得到。

Check Box 和 Radio Button

因為都是屬於 Button 型別 , 可以通過點選來選中 Check Box 和 Radio Button 。但通過使用 IWindow 提供的方法,不能知道控制元件是否被選中(進一步處理請參照後面的“擴充套件 IWindow 功能”部分)。

Text Field

通常我們需要獲得控制元件上顯示的文字和往控制元件上輸入文字。輸入文字可以直接使用 RFT 使用者熟悉的一些方法,具體就是先呼叫 IWindow ’ 的 click 方法,然後使用 IScreen 的 inputKeys() 方法來輸入文字 (RationalTestScript.getScreen().inputKeys())。得到 Text Field 上顯示的文字需要一點技巧:先選中文字,再複製到剪下板裡,然後使用 Java 的功能讀取剪下板裡的文字。下面是具體實現的程式碼:


例 1: 讀取 text field 上顯示的文字
public String getText() { 
        IWindow win = this.getControl(); 
        IScreen screen = RationalTestScript.getScreen(); 
        
        //copy text to clipboard 
        win.click(); 
        screen.inputKeys("{HOME}"); 
        screen.inputKeys("+{END}"); 
        screen.inputKeys("^C"); 
        
        //then get string off of clipboard and return it 
        String sReturn = ""; 
        
        Transferable t = Toolkit.getDefaultToolkit() 
                             .getSystemClipboard().getContents(null); 
        
        if (t==null) //nothing on clipboard 
            return ""; 
        
        try { 
            sReturn = t.getTransferData( 
                           DataFlavor.stringFlavor).toString(); 
        } 
        catch (IOException e) 
        { 
            System.err.println( 
                      "getText threw IOException; returning empty string."); 
            System.err.println("Error = " +  e); 
            e.printStackTrace(); 
        } 
        catch (java.awt.datatransfer.UnsupportedFlavorException ufe) 
        { 
            System.err.println("getText threw UnsupportedFlavorException; 
            i.e. data on clipboad is not string data; returning empty string."); 
            System.err.println("Error = " +  ufe); 
            ufe.printStackTrace(); 
        } 
        return sReturn; 
        
    }

Combo Box 和 List Box

Combo Box 是文字框和下拉選單的一個組合控制元件,文字框的部分可以按上面的步驟當作 Text Field 來處理。但是通過使用 IWindow 並不能得到下拉選單的內容,此外,List Box 比 Combo box 更棘手,不能通過輸入文字來選擇,所以需要一些手段來繞過這些限制。最好的方式是使用鍵盤來選擇 List Box 中的條目。當然,需要知道條目的順序才能選擇正確。下面是一個使用 IWindow 來選擇 Combo Box 下拉選單的例子


例 2: 選擇 Combo Box 下拉選單
public void selectWithKeys(int index) 
    { 
        this.showDropDown(); 
        IScreen screen = RationalTestScript.getScreen(); 
        screen.inputKeys("{HOME}");        
        for (int i = 0; i

因為 IWindow 獲取控制元件資訊存在上文提到的一些限制,最好不要用 IWindow 來驗證 GUI 的測試點,但可以用它來操作 SUT 到達測試的狀態。舉例來說,很難利用 IWindow 來驗證 Combo Box 的狀態,因為只用 IWindow 不容易得到 Combo Box 的所有資訊,但是可以使用 IWindow 來設定應用程式的狀態。例如,設定瀏覽器選項或者匯出 html 到檔案進行驗證,等等。

經上所述,得到控制元件的 IWindow 之後,可以直接對控制元件進行基本的操作,關鍵在於如何找到所需的控制元件。下面兩章會分別在 Windows 和 Linux 上做進一步探討。

通過 IWindow 查詢 Windows 窗體和控制元件

在探討 Linux 的特殊之處前,先介紹一下如何在 Windows 上查詢原生控制元件。一方面出於教學目的,先理解 IWindow 上的工作原理,有助於理解 Linux 上的基本用法,因為 Linux 上稍微複雜一點。另一方面,如果在 Linux 和 Windows 上測試同一個軟體,會希望在兩個平臺上使用相同的介面來減少維護的工作。(當然,如果只需要測試 Windows 上的原生控制元件,使用 RFT 的 TestObject 介面會更容易些)

為了使用 IWindow 查詢控制元件,首先需要找到他的父窗體,然後再遍歷這個窗體的所有子控制元件,從中找到匹配條件的。父窗體通常是對話方塊或者被測程式的主窗體,都可以通過標題找到。例如,可以用下面的方法找到對話方塊上的控制元件。


例 3: 通過標題找對話方塊

public IWindow getDialog(String caption) 
    { 
        IWindow[] wins = RationalTestScript.getTopWindows(); 
        for (int i = 0; i

例 3 中的方法遍歷螢幕上所有的頂級窗體,得到每個窗體的標題,當找到匹配標題的時候退出迴圈 (*2) 。

得到頂級窗體之後,繼續搜尋它的子控制元件。有幾種屬性可以用來定位控制元件。比如,Button 通常能通過文字找到,因此可以用下面的方式使用指定的文字搜尋控制元件。


例 4: 利用文字查詢控制元件

public IWindow findControl(String caption) 
    { 
        
        //find parent window    
        IWindow parentWindow = getDialog("My Dialog Caption"); 
        if (parentWindow == null) 
            return null; 
        
        //else find control 
        IWindow children[] = parentWindow.getChildren();                
        // if there are no children for this control, then control can't be found 
        if (children.length == 0) 
            return null; 
        
        for (int i = 0; i < children.length; i++) { 
            if (children[i].getText().equals(caption)) 
                return children[i]; 
        } 
        // no match to caption was found 
        return null; 
    }

這個例子遍歷頂級窗體的所有子控制元件,查詢給定標題的按鈕 (*3) 。搜尋文字對某些控制元件有效,但不是所有控制元件都有對應的文字。比如 Text Field 就沒有對應的文字(IWindow#getText 並不返回 Text Field 上的內容,只能得到對應 window 上的文字屬性,是個空字串)。對於 List Box 也是這樣。因此,我們需要其它的方法處理這類控制元件。一個方法就是使用索引定位這類控制元件,下面是一個使用索引查詢控制元件的模式。


例 5: 通過索引查詢控制元件

public IWindow findControl(String clazz, int index) 
    { 
        int count = 0; 
        
        //find parent window    
        IWindow parentWindow = getDialog("My Dialog Caption"); 
        if (parentWindow == null) 
            return null; 
        
        //else find control 
        IWindow children[] = parentWindow.getChildren();                
        // if there are no children for this control, then control can't be found 
        if (children.length == 0) 
            return null; 
        
        for (int i = 0; i < children.length; i++) { 
            if (children[i].getWindowClass().equals(clazz)) { 
                count++; 
                if (count == index) 
                    return children[i]; 
            } 
        } 
        // no match to caption was found 
        return null; 
    }

這個模式使用了一個類名和一個表示控制元件索引的整形數。它遍歷所有控制元件,當找到指定類名的控制元件時,計數器加一。當計數器與索引值相等時,返回當前的 IWindow 例項,這裡注意 Windows 上的標準控制元件包括 Button, ComboBox, ListBox, 和 Edit 。因為 Button 包括 Check Box 和 Radio Button,所有種類的 Button 必須用同一個類的索引。需要補充的是 Text Fields 屬於 Edit 型別。

但使用索引存在一個問題 ,當控制元件發生移動時(比如研發人員決定在對話方塊上增加一個 Text Field), 索引值會變化,這樣就需要修改測試程式碼。更可靠的一個方式是使用控制元件的 ID, 通過 IWindow#getId() 可以得到。控制元件 ID 是 Windows 賦給每個控制元件的編號,通常在相應對話方塊上是唯一的。所以無論控制元件怎麼移動,控制元件 ID 是不會變的。不幸的是,控制元件 ID 只在 Windows 平臺上有,由於本文的重點在 Linux 上,不詳述控制元件 ID 。

通過 IWindow 查詢 Linux 窗體

Linux 平臺上面有一些複雜。X window 系統有一個客戶機和伺服器(Client-Server)結構。客戶機(應用程式本身)決定螢幕上應該顯示什麼,伺服器根據客戶機提供的資料在螢幕上畫出相應內容,並且把使用者的輸入傳送到客戶機(現在的 Linux 上面,客戶機和伺服器是同一臺機器)。GUI 程式通過 Xlib 提供的函式來操作客戶機和伺服器。在 Xlib 這個層次,螢幕上顯示的元素是一個個窗體,每個螢幕上的最頂級窗體叫做根窗體 (root window), 它覆蓋了整個螢幕。其它的窗體都是根窗體的直接或間接子窗體。應用程式的主窗體和對話方塊之間沒有主從關係。它們都是根窗體 的直接子窗體。由於這個原因,IWindow#getOwner 方法在 Linux 上只返回 null.

控制元件是客戶端上的物件,在伺服器端有一個對應的窗體。它把顯示的內容畫在窗體的區域裡,並且響應窗體的事件。客戶端對應每個窗體都有一個叫做 Graphic Context (GC) 的資料結構,該結構儲存窗體上需要顯示的資料。畫新窗體或者重畫窗體的時候,GC 都需要被髮送給伺服器。伺服器根據 GC 的內容將窗體顯示出來,但是並不儲存 GC 。伺服器僅僅只儲存窗體和他們的一些屬性在記憶體中。這些屬性通過傳遞給窗體管理器(window manager)來決定窗體的行為,但不包括窗體上顯示的內容。儘管 Xlib 提供一些函式,能夠通過窗體的 ID 獲取屬性,但是我們只能得到關於窗體顯示內容有限的一點資訊,因為決定窗體顯示內容的資訊儲存在應用程式裡,這些資料不僅包括 GC, 還包括定義在更高層 lib 中的控制元件屬性。這就是為什麼 RFT 在 Linux 上處理原生控制元件時有很多侷限性。在 Linux 上有效的自動測試工具需要通過 IPC 的方法在 SUT 中開放一些介面,或者,使用 Accessibility 相關的介面,如 Accessibility Tool Kit (ATK),來獲得控制元件型別和文字資訊。通過 IWindow 可以得到的資訊包括:1,窗體標題 (窗體的 WM_NAME 屬性)、2 ,窗體的大小和位置、3,窗體名和類名 (窗體的 WM_CLASS 屬性)。

RFT 有幾種方可以得到 IWindow (TestWindow 的例項 ) 。下面的部分使用 Firefox 的“ Open File ”對話方塊為例,來演示如何在 Windows 和 Linux 上用相同的方式使用 IWindow 的函式。


圖 1. Firefox 在 Metacity 窗體管理器環境中的檔案對話方塊
Firefox 在 Metacity 窗體管理器環境中的檔案對話方塊

點選檢視大圖

1. 獲得當前活動的窗體

使用 RationalTestScript.getScreen().getActiveWindow() 函式可以直接得到當前活動的窗體。它在 Windows 平臺上返回正確的 TestWindow 例項,但在 Linux 上它有可能是窗體管理器建立的包裝窗體 (Wrapper Window) 。GNOME 上的預設窗體管理器是 Metacity,會在應用程式窗體建立的時候自動加上一個包裝窗體。包裝窗體是應用程式窗體的父窗體,除了包括應用程式窗體本身外,還包括標題欄,圖示,按鈕和邊框。getActiveWindow 方法在這種情況下返回包裝窗體的 IWindow 。因此我們需要呼叫 IWindow 的 getChildren 來得到應用程式窗體。GNOME 還有另外一個窗體管理器— Compiz. 它會在桌面特效 (XGL) 開啟的時候代替 Metacity 。Compiz 並不需要包裝窗體來顯示窗體的邊框,它使用叫做 window decorator 的程式來給每個應用程式窗體顯示邊框。getActiveWindow 方法在 Compiz 的環境下可以跟 Windows 平臺一樣,直接得到應用程式的 IWindow 。

包裝窗體有幾個特殊的屬性可以讓我們識別:1,僅有一個子窗體,該子窗體就是應用程式窗體、2,窗體名字是空的、3,WM_CLASS 屬性沒有設定。例 6 說明了如何通過檢查 WM_CLASS 屬性和它的子窗體數目來跳過包裝窗體。WM_CLASS 屬性和 getWindowClasName 方法的關係會在下面一節描述(*4)。


例 6: 按需要跳過包裝窗體

public IWindow skipWrapper(IWindow current){ 
        if (current.getWindowClassName().equals("") 
               && current.getChildren().length == 1) 
            current = current.getChildren()[0]; 
        return current; 
    }

2. 同過標題和類名來查詢窗體

每個窗體或對話方塊都有一個屬性來儲存標題,IWindow 的 getText 方法根據這一屬性來獲得標題文字。窗體的類名在 Windows 平臺上用來描述窗體的型別,比如 "#32770" 代表對話方塊。X Window 也有一個相似的屬性,就是前面提過的 WM_Class. 它包括兩個字串:res_name 和 res_class 。res_name 指的是應用程式名,res_class 代表窗體的類名。getWindowClassName 方法把兩個字串合成一個作為結果返回,比如,圖 1 中的檔案對話方塊呼叫 getWindowClassName 返回 "Firefox-bin Gecko" 。使用 getTopWindows 可以得到系統中的所有頂級窗體(Linux 上不包括最小化的窗體),通過窗體的標題和類名可以定位我們要找的窗體或對話方塊。例 7 演示如何在 Windows 和 Linux 上獲取檔案對話方塊的 IWindow 。


例 7: 得到指定的對話方塊

public final boolean IS_WINDOWS = 
                         RationalTestScript.getOperatingSystem().isWindows(); 
     public String sCaption = "Open File"; 
     public String sWindowClassName =  IS_WINDOWS? "#32770":"Firefox-bin Gecko"; 

     public IWindow getWindow(){ 
        IWindow[] wins = RationalTestScript.getTopWindows(); 
        IWindow current = null; 
        for (int i = (IS_WINDOWS ? 0 : (wins.length - 1)); 
            (IS_WINDOWS ? i < wins.length : i >= 0); 
            i = i + (IS_WINDOWS ? 1 : -1)) 
        { 
            current = skipWrapper(wins[i]); 
            if (current.getText().matches(sCaption) && 
                     current.getWindowClassName().matches(sWindowClassName)) 
                break; 
        } 
        return current; 
    }

例 7 中,在 Windows 和 Linux 上使用相反的順序遍歷窗體,這跟 getTopWindows 函式在兩個平臺上返值得順序有關:Windows 上新開啟的窗體在返回的數前面,而 Linux 上新開啟的排在後面。使用合適的遍歷順序,一方面,當有兩個或多個標題相同的窗體時,可以返回最後開啟的那個(通常是我們要測試的),另一方面,可以更快定位到我們需要的窗體。例 7 中還用到了例 6 中定義的函式,在需要的情況下跳過包裝窗體。

找到窗體的 IWindow 之後,通常需要做的第一件事是啟用該窗體,但是 IWindow#activate 方法在 Linux 上的 Metacity 窗體管理器環境下無效。例 8 演示了通過點選窗體上的一個點來啟用該窗體,從而繞過這一限制。getScreenRectangle 方法可以得到包括窗體大小和位置的 Rectangle 物件,它在 Linux 上並不包括標題欄和邊框,相信看過前面對窗體管理器的介紹,不難理解原因。為了得到跟 Windows 上一樣,包括窗體標題欄和邊框的 Rectangle, 在 Metacity 環境下,可以通過 IWindow.getParent().getScreenRectangle() 得到包裝窗體的 Rectangle 。但是在 Compiz 環境下沒有辦法得到,因為標題欄和邊框並不是顯示在窗體內部。


例 8: 啟用一個窗體 :

IWindow current = getWindow(); 
 public void setActive(){ 
        Rectangle r = current.getScreenRectangle(); 
        getScreen().click(atPoint(r.x, r.y)); 
 }

IWindow 還提供了 close 方法,但是隻對頂級窗體有效。在 Metacity 環境下會丟擲 UnsupportedMethodException 的異常,因為它認為應用程式窗體不是頂級窗體。為了關掉窗體,可以把 IWindow 轉換成 TopLevelWindow,然後呼叫 close 函式。需要注意的是,TopLevelWindow 是 RFT 內部使用的一個類,通常在 SUT 中建立,被 RFT 的 proxy 物件呼叫。如果在測試指令碼中建立它,只有一部分函式可以使用,比如 close, 因為它的很多函式只對自己的程式有效。


例 9: 關閉窗體

IWindow current = getWindow(); 
 new TopLevelWindow(current.getHandle()).close();

TopLevelWindow#setFocus 可以被用來在 Linux 上啟用窗體,同樣也適用於 Windows 。例 10 是另外一個啟用窗體的例子,相比例 8,窗體能在被其它窗體遮擋的時候被啟用。

例 10: 使用 TopLevelWindow 在 Linux 和 Windows 上啟用窗體

TopLevelWindow(current.getHandle()).setFocus();

一旦在 Linux 上找到控制元件,同樣可以應用上面的方法來操作那個控制元件,取決於控制元件的型別。也就是說,可以像本文第一章講的那樣,點選 Button, Check Box, 和 Radio Button, 在 Text Field 上輸入和讀取文字,和選擇 List Box 的條目。但是僅用 IWindow 的函式,在 Linux 上還不能找到控制元件,需要使用下一章提到的方式來獲取控制元件的類名和相關資訊。

擴充套件 IWindow 的功能

上面提到過 ,IWindow 本身只提供一些基本的介面來操作原生控制元件,靠這些介面來測試還有很多侷限性,但是可以通過一些方式來擴充套件 RFT,滿足測試原生控制元件的所有需求 。比如前面提到的兩點:無法得到 Check Box 或 Radio Button 的狀態、不能得到 List Box 的內容。通過使用 JNI 的程式碼,可以解決這些問題。

同樣,這些擴充套件在 Windows 上更簡單和易於理解。Win32 API 提供了一些可以得到 Check Box 或 Radio Button 的狀態、 List Box 內容的函式。因此可以寫 JNI 調來呼叫這些 Win32 API 得到需要的這些值。

不幸的是,這些擴充套件在 Linux 上更復雜一些。原因在於 Linux widget API 只在執行函式的程式中有效。因此,不僅需要提供一些 JNI 介面實現資料列集(marshalling),和跟 SUT 程式通訊的機制,還需要把程式碼放在 SUT 的程式中去呼叫合適的 widget API, 並把需要的結果返回出來。

舉例來說,對使用 GTK 控制元件的 SUT,我們需要在 SUT 的程式中使用下面的 C 程式碼,來得到 Check box 的狀態。


例 11: 得到 Check box 的狀態

BOOL GtkToggleItemGetStateFromXID (long xid) 
 { 
    GdkWindow *w = NULL; 
    GtkWidget *control = NULL; 
    gboolean bReturn=FALSE; 

    HoldChamixGtkLock(); //lock before calling x 

    w = gdk_window_lookup((Window)xid); 
    if (w!=NULL) 
        gdk_window_get_user_data( w, (gpointer *)&control); 
       //get the GtkWidget * of the control 

    if (control !=NULL && GTK_TOGGLE_BUTTON (control)) 
        bReturn = gtk_toggle_button_get_active (control); 

    ReleaseChamixGtkLock(); 
    
    return bReturn; 
 }

在 SUT 的程式中使用上面的函式得到需要的值後,還需要把資訊傳遞給 RFT 。要實現這一點,需要用 IPC 讓 SUT 和 JNI 程式碼通訊。有很多方法可以建立 IPC 傳遞這樣的資訊,具體的實現方法不在本文探討的範圍中。

IPC 在 Windows 原生控制元件的測試方面也十分有用。儘管可以呼叫 Win32 API 來獲取 Windows 標準控制元件的資訊,但是它對定製或非標準的控制元件並不湊效。以 Lotus Notes 的 "Rich Text Field" 控制元件為例,它有可能包含 hotspot,OLE 物件,帶格式的文字和其他很多富文字專案 , Windows 把整個 Rich Text Field 看作一個大矩形,並不知道如何操作裡面的元素,比如 Hotspots 或連結。假如有了 IPC 的機制,可以直接跟 SUT 通訊來操作定製的控制元件。例如,可以在 SUT 中寫一小段程式碼來返回一個 Rectangle, 它代表 Rich Text Field 裡面 Hotspot 的大小和位置,當需要點選 Hotspot 的時候,通過 JNI 和 IPC 在 RFT 中呼叫這段程式碼來得到這個 Rectangle, 使用 RFT 來點選它代表的點。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/14780914/viewspace-588980/,如需轉載,請註明出處,否則將追究法律責任。

相關文章